diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000000..bce33191f8
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,2 @@
+export NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://cache.nickthesick.com
+export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=g8@ucL8em4*Z9TKXDY9OEX@!upf^Nz9
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000..e9511bbf61
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,42 @@
+{
+ "root": true,
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "react-app",
+ "react-app/jest"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["import", "@typescript-eslint"],
+ "settings": {
+ "import/extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx"],
+ "import/external-module-folders": ["node_modules", "node_modules/@types"],
+ "import/parsers": {
+ "@typescript-eslint/parser": [".ts", ".cts", ".mts", ".tsx"]
+ },
+ "import/resolver": {
+ "node": {
+ "extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx"]
+ }
+ }
+ },
+ "ignorePatterns": ["**/ui/*"],
+ "rules": {
+ "no-console": "error",
+ "curly": 1,
+ "import/extensions": ["error", "always", { "ignorePackages": true }],
+ "import/no-extraneous-dependencies": [
+ "error",
+ {
+ "devDependencies": true,
+ "peerDependencies": true,
+ "optionalDependencies": false,
+ "bundledDependencies": false
+ }
+ ],
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "import/no-cycle": "error"
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000000..372e429b34
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,101 @@
+name: Bug report
+description: Report a bug or broken behavior in BlockNote
+labels:
+ - bug
+ - needs-triage
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for reporting a bug!
+ Please use this template to describe **broken or incorrect behavior**.
+
+ Feature ideas or questions should go to **Discussions**.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What’s broken?
+ description: >
+ Describe the problem clearly and concisely.
+ What is happening that should not be happening?
+ placeholder: >
+ Example:
+ When editing a table cell and pressing Enter, the editor crashes and the document cannot be recovered.
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: What did you expect to happen?
+ description: >
+ Describe the expected or correct behavior.
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: >
+ Provide clear steps so we can reproduce the issue.
+ If possible, an online reproduction (e.g. StackBlitz) is extremely helpful.
+ placeholder: |
+ 1. Create a new document
+ 2. Insert a table
+ 3. Click inside a cell
+ 4. Press Enter
+
+ Optional: If you can, provide a minimal online reproduction.
+ You can use this starter sandbox:
+ https://stackblitz.com/github/TypeCellOS/BlockNote/tree/main/examples/01-basic/01-minimal?file=App.tsx
+ validations:
+ required: false
+
+ - type: input
+ id: version
+ attributes:
+ label: BlockNote version
+ description: >
+ Optional — specify the version you’re using, if known.
+ placeholder: e.g. v0.18.2
+ validations:
+ required: false
+
+ - type: input
+ id: environment
+ attributes:
+ label: Environment
+ description: >
+ Browser, OS, framework, or other relevant environment details.
+ placeholder: e.g. Chrome 121, macOS 14, React 18
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional context
+ description: >
+ Screenshots, videos, logs, or any other context that might help.
+ validations:
+ required: false
+
+ - type: checkboxes
+ id: contribute
+ attributes:
+ label: Contribution
+ options:
+ - label: "I'd be interested in contributing a fix for this issue"
+ required: false
+
+ - type: checkboxes
+ id: sponsor
+ attributes:
+ label: Sponsor
+ description: >
+ Optional — helps us prioritize first response according to our SLA.
+ options:
+ - label: "I'm a [sponsor](https://www.blocknotejs.org/pricing) and would appreciate if you could look into this sooner than later 💖"
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..f258f05126
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,6 @@
+blank_issues_enabled: false
+
+contact_links:
+ - name: Share an idea or suggest an enhancement
+ url: https://github.com/TypeCellOS/BlockNote/discussions/categories/ideas-enhancements
+ about: Share feature ideas, enhancement suggestions, or other ideas for the BlockNote project.
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..165dad4e01
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,57 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ versioning-strategy: "lockfile-only"
+ directories:
+ - "/"
+ - "/packages/*"
+ schedule:
+ interval: "weekly"
+ labels:
+ - "npm dependencies"
+ commit-message:
+ prefix: "chore"
+ allow:
+ # @tiptap packages
+ - dependency-name: "@tiptap/core"
+ - dependency-name: "@tiptap/pm"
+ - dependency-name: "@tiptap/react"
+ - dependency-name: "@tiptap/extensions"
+ - dependency-name: "@tiptap/extension-bold"
+ - dependency-name: "@tiptap/extension-code"
+ - dependency-name: "@tiptap/extension-horizontal-rule"
+ - dependency-name: "@tiptap/extension-italic"
+
+ - dependency-name: "@tiptap/extension-paragraph"
+ - dependency-name: "@tiptap/extension-strike"
+ - dependency-name: "@tiptap/extension-text"
+ - dependency-name: "@tiptap/extension-underline"
+ # prosemirror packages
+ - dependency-name: "prosemirror-changeset"
+ - dependency-name: "prosemirror-highlight"
+ - dependency-name: "prosemirror-model"
+ - dependency-name: "prosemirror-state"
+ - dependency-name: "prosemirror-tables"
+ - dependency-name: "prosemirror-transform"
+ - dependency-name: "prosemirror-view"
+ # react packages
+ - dependency-name: "react"
+ - dependency-name: "react-dom"
+ # yjs packages
+ - dependency-name: "yjs"
+ - dependency-name: "y-prosemirror"
+ ignore:
+ # Hono packages are used only in the demo AI server and are not part of
+ # the main editor/runtime surface area.
+ - dependency-name: "hono"
+ - dependency-name: "@hono/node-server"
+ - dependency-name: "@hono/*"
+ groups:
+ editor-dependencies:
+ patterns:
+ - "@tiptap/*"
+ - "prosemirror-*"
+ - "react"
+ - "react-dom"
+ - "yjs"
+ - "y-prosemirror"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..9870532e04
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,34 @@
+# Summary
+
+
+
+## Rationale
+
+
+
+## Changes
+
+
+
+## Impact
+
+
+
+## Testing
+
+
+
+## Screenshots/Video
+
+
+
+## Checklist
+
+- [ ] Code follows the project's coding standards.
+- [ ] Unit tests covering the new feature have been added.
+- [ ] All existing tests pass.
+- [ ] The documentation has been updated to reflect the new feature
+
+## Additional Notes
+
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bef6fe05f7..91b5ca0414 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,76 +5,203 @@ on:
- main
pull_request:
types: [opened, synchronize, reopened, edited]
- branches:
- - main
- - "project/**"
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }}
+ NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }}
+ pnpm_config_store_dir: ./node_modules/.pnpm-store
jobs:
build:
name: Build
runs-on: ubuntu-latest
+ timeout-minutes: 60
steps:
- - uses: actions/checkout@v2
-
- - uses: actions/setup-node@v2
+ - uses: actions/checkout@v6
with:
- node-version: "18.x"
+ fetch-depth: 100
- - name: Cache node modules
- uses: actions/cache@v2
- env:
- cache-name: cache-node-modules
+ - name: Install pnpm
+ uses: pnpm/action-setup@v5
+
+ - uses: nrwl/nx-set-shas@v5
+
+ - uses: actions/setup-node@v6
with:
- # npm cache files are stored in `~/.npm` on Linux/macOS
- path: ~/.npm
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
+ cache: "pnpm"
+ cache-dependency-path: "**/pnpm-lock.yaml"
+ node-version-file: ".nvmrc"
- - name: cache playwright
- id: playwright-cache
- uses: actions/cache@v2
+ - name: Cache NX
+ uses: actions/cache@v5
with:
- path: ~/.cache/ms-playwright
- key: pw-new-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
+ path: .nx/cache
+ key: nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }}
+ restore-keys: |
+ nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-
+ nx-${{ env.NX_BRANCH }}-
+ nx-
- name: Install Dependencies
- run: npm install
-
- - name: Bootstrap packages
- run: npm run bootstrap
+ run: pnpm install
- name: Lint packages
- run: npm run lint
- env:
- CI: true
+ run: pnpm run lint
- name: Build packages
- run: npm run build
- env:
- CI: true
+ run: pnpm run build
- name: Run unit tests
- run: npm run test
- env:
- CI: true
+ run: pnpm run test
+
+ - name: Run Next.js integration test (production build)
+ run: NEXTJS_TEST_MODE=build npx vitest run tests/src/unit/nextjs/serverUtil.test.ts
+
+ - name: Upload webpack stats artifact (editor)
+ uses: relative-ci/agent-upload-artifact-action@v2
+ with:
+ webpackStatsFile: ./playground/dist/webpack-stats.json
+ artifactName: relative-ci-artifacts-editor
+
+ - name: Soft release
+ id: soft-release
+ run: pnpx pkg-pr-new publish './packages/*' # TODO disabled only for AI branch--compact
+
+ playwright-build:
+ name: "Playwright Build"
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 100
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v5
+
+ - uses: nrwl/nx-set-shas@v5
+
+ - uses: actions/setup-node@v6
+ with:
+ cache: "pnpm"
+ cache-dependency-path: "**/pnpm-lock.yaml"
+ node-version-file: ".nvmrc"
+
+ - name: Cache NX
+ uses: actions/cache@v5
+ with:
+ path: .nx/cache
+ key: nx-playwright-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }}
+ restore-keys: |
+ nx-playwright-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-
+ nx-playwright-${{ env.NX_BRANCH }}-
+ nx-
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Build packages
+ run: pnpm run build
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: playwright-build
+ path: |
+ packages/*/dist
+ playground/dist
+ retention-days: 1
+
+ playwright:
+ name: "Playwright Tests - ${{ matrix.browser }} (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})"
+ runs-on: ubuntu-latest
+ needs: playwright-build
+ timeout-minutes: 30
+ container:
+ image: mcr.microsoft.com/playwright:v1.51.1-noble
+ strategy:
+ fail-fast: false
+ matrix:
+ browser: [chromium, firefox, webkit]
+ shardIndex: [1, 2]
+ shardTotal: [2]
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 100
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v5
+
+ - uses: actions/setup-node@v6
+ with:
+ cache: "pnpm"
+ cache-dependency-path: "**/pnpm-lock.yaml"
+ node-version-file: ".nvmrc"
+
+ - name: Download build artifacts
+ uses: actions/download-artifact@v8
+ with:
+ name: playwright-build
+
+ - name: Install dependencies
+ run: pnpm install
- - name: Run server
- run: npm run start:built & npx wait-on http://localhost:3000
- env:
- CI: true
+ - name: Run server and Playwright tests
+ run: |
+ HOME=/root PLAYWRIGHT_CONFIG="--project ${{ matrix.browser }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}" pnpm run e2e
- - name: Install Playwright
- run: npx playwright install --with-deps
+ - name: Upload blob report
+ uses: actions/upload-artifact@v7
+ if: ${{ !cancelled() }}
+ with:
+ name: blob-report-${{ matrix.browser }}-${{ matrix.shardIndex }}
+ path: tests/blob-report/
+ retention-days: 1
+
+ - name: Upload HTML report
+ uses: actions/upload-artifact@v7
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report-${{ matrix.browser }}-${{ matrix.shardIndex }}
+ path: tests/playwright-report/
+ retention-days: 30
+
+ merge-reports:
+ name: "Merge Playwright Reports"
+ if: ${{ !cancelled() }}
+ needs: playwright
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v5
+
+ - uses: actions/setup-node@v6
+ with:
+ cache: "pnpm"
+ cache-dependency-path: "**/pnpm-lock.yaml"
+ node-version-file: ".nvmrc"
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Download blob reports
+ uses: actions/download-artifact@v8
+ with:
+ path: tests/all-blob-reports
+ pattern: blob-report-*
+ merge-multiple: true
- - name: Run Playwright tests
- run: npx playwright test
+ - name: Merge reports
+ run: npx playwright merge-reports --reporter html ./all-blob-reports
+ working-directory: tests
- - uses: actions/upload-artifact@v2
- if: always()
+ - name: Upload merged HTML report
+ uses: actions/upload-artifact@v7
with:
- name: playwright-report
- path: playwright-report/
+ name: playwright-report-merged
+ path: tests/playwright-report/
retention-days: 30
diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml
new file mode 100644
index 0000000000..6d6ed4a452
--- /dev/null
+++ b/.github/workflows/fresh-install-tests.yml
@@ -0,0 +1,144 @@
+name: Fresh Install Tests
+
+# Periodically tests BlockNote with the latest versions of its production
+# dependencies (within declared semver ranges). This catches breakage when a
+# new release of a dep like @tiptap/* or prosemirror-* ships and conflicts
+# with BlockNote's declared ranges — the kind of failure a user would hit when
+# running `npm install @blocknote/react` in a fresh project.
+#
+# Only production dependencies of published (non-private) packages are updated.
+# DevDependencies (vitest, vite, typescript, etc.) stay pinned to the lockfile,
+# so test tooling churn doesn't cause false positives.
+
+on:
+ schedule:
+ - cron: "0 2 * * *" # Daily at 02:00 UTC
+ workflow_dispatch: # Allow manual runs
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ pnpm_config_store_dir: ./node_modules/.pnpm-store
+
+jobs:
+ fresh-install-unit-tests:
+ name: Unit Tests (Fresh Dep Resolution)
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - id: checkout
+ uses: actions/checkout@v6
+
+ - id: install_pnpm
+ name: Install pnpm
+ uses: pnpm/action-setup@v5
+
+ - id: setup_node
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: ".nvmrc"
+ # Intentionally no pnpm cache — we want fresh prod dep resolution
+
+ - id: install_dependencies
+ name: Install dependencies
+ run: pnpm install
+
+ - id: update_prod_deps
+ name: Update prod deps of published packages
+ # Resolves production dependencies of every published (non-private)
+ # workspace package to the latest version within their declared semver
+ # ranges. This simulates what a user gets when running
+ # `npm install @blocknote/react` in a fresh project.
+ # DevDependencies are left at their lockfile versions.
+ run: |
+ FILTERS=$(node -e "
+ const fs = require('fs');
+ const path = require('path');
+ fs.readdirSync('packages').forEach(dir => {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join('packages', dir, 'package.json'), 'utf8'));
+ if (!pkg.private && pkg.name) process.stdout.write('--filter ' + pkg.name + ' ');
+ } catch {}
+ });
+ ")
+ echo "Updating prod deps for: $FILTERS"
+ eval pnpm update --prod $FILTERS
+
+ - id: dedupe_deps
+ name: Dedupe transitive dependencies
+ # After bumping the publishable packages' prod deps, collapse any
+ # duplicate transitive resolutions (e.g. @tiptap/core + @tiptap/pm)
+ # that would otherwise differ between the updated publishable packages
+ # and the un-updated examples/playground. Without this, TypeScript
+ # treats the two copies' exports as unrelated types and example-editor
+ # fails to build (TS2322 on Extension vs AnyExtension).
+ # Dedupe only rewrites the lockfile — it does NOT modify package.json,
+ # so the examples' "@blocknote/*": "latest" specs (which is what
+ # CodeSandbox users see) stay intact.
+ run: pnpm dedupe
+
+ - id: build_packages
+ name: Build packages
+ run: pnpm run build
+ env:
+ NX_SKIP_NX_CACHE: "true"
+
+ - id: run_unit_tests
+ name: Run unit tests
+ run: pnpm run test
+ env:
+ NX_SKIP_NX_CACHE: "true"
+
+ - name: Notify Slack on workflow failure
+ if: ${{ failure() }}
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+ REPOSITORY: ${{ github.repository }}
+ WORKFLOW: ${{ github.workflow }}
+ RUN_ID: ${{ github.run_id }}
+ RUN_NUMBER: ${{ github.run_number }}
+ RUN_ATTEMPT: ${{ github.run_attempt }}
+ BRANCH: ${{ github.ref_name }}
+ run: |
+ if [ -z "$SLACK_WEBHOOK_URL" ]; then
+ echo "SLACK_WEBHOOK_URL is not configured; skipping Slack notification."
+ exit 0
+ fi
+
+ failed_step="Unknown step"
+ if [ "${{ steps.checkout.outcome }}" = "failure" ]; then
+ failed_step="Checkout repository"
+ elif [ "${{ steps.install_pnpm.outcome }}" = "failure" ]; then
+ failed_step="Install pnpm"
+ elif [ "${{ steps.setup_node.outcome }}" = "failure" ]; then
+ failed_step="Setup Node.js"
+ elif [ "${{ steps.install_dependencies.outcome }}" = "failure" ]; then
+ failed_step="Install dependencies"
+ elif [ "${{ steps.update_prod_deps.outcome }}" = "failure" ]; then
+ failed_step="Update prod deps of published packages"
+ elif [ "${{ steps.dedupe_deps.outcome }}" = "failure" ]; then
+ failed_step="Dedupe transitive dependencies"
+ elif [ "${{ steps.build_packages.outcome }}" = "failure" ]; then
+ failed_step="Build packages"
+ elif [ "${{ steps.run_unit_tests.outcome }}" = "failure" ]; then
+ failed_step="Run unit tests"
+ fi
+
+ run_url="https://github.com/${REPOSITORY}/actions/runs/${RUN_ID}"
+ message=$(printf '%s\n%s\n%s\n%s' \
+ ":warning: Fresh Install Tests failed in *${REPOSITORY}* on branch *${BRANCH}*." \
+ "*Workflow:* ${WORKFLOW}" \
+ "*Run:* <${run_url}|#${RUN_NUMBER} (attempt ${RUN_ATTEMPT})>" \
+ "*Failed step:* ${failed_step}")
+ payload=$(jq --compact-output --null-input --arg text "$message" '{text: $text}')
+
+ curl -sS -X POST \
+ --fail \
+ --retry 4 \
+ --retry-all-errors \
+ --retry-max-time 60 \
+ --connect-timeout 10 \
+ --max-time 30 \
+ -H "Content-type: application/json" \
+ --data "$payload" \
+ "$SLACK_WEBHOOK_URL"
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 0000000000..280d5a5af1
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,74 @@
+# ./.github/workflows/publish.yml
+name: Publish
+
+on:
+ push:
+ tags:
+ - v*.*.*
+ - v*.*.*-*
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version tag to publish (e.g., v0.x.x-hotfix)"
+ required: true
+ type: string
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }}
+ NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }}
+ pnpm_config_store_dir: ./node_modules/.pnpm-store
+
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write # needed for provenance data generation
+ attestations: write
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ inputs.version }}
+ fetch-depth: 100
+ - name: Display version being published
+ run: |
+ echo "Publishing version: ${{ inputs.version }}"
+
+ - run: jq '.packageManager' package.json | tr -d '"pnpm@'
+ id: package-manager-version
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v5
+ with:
+ version: ${{ steps.package-manager-version.outputs.stdout }}
+
+ - uses: nrwl/nx-set-shas@v5
+
+ - uses: actions/setup-node@v6
+ with:
+ cache: "pnpm"
+ cache-dependency-path: "**/pnpm-lock.yaml"
+ node-version-file: ".nvmrc"
+
+ - name: Cache NX
+ uses: actions/cache@v5
+ with:
+ path: .nx/cache
+ key: nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }}
+ restore-keys: |
+ nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-
+ nx-${{ env.NX_BRANCH }}-
+ nx-
+
+ - name: Install Dependencies & Build
+ run: pnpm install && pnpm build
+
+ - name: Print Environment Info
+ run: pnpm exec nx report
+
+ - name: Publish packages
+ # Ensure npm 11.5.1 or later for trusted publishing
+ run: npm install -g npm@latest && pnpm exec nx release publish --access public
diff --git a/.github/workflows/relative-ci.yaml b/.github/workflows/relative-ci.yaml
new file mode 100644
index 0000000000..c2b848e202
--- /dev/null
+++ b/.github/workflows/relative-ci.yaml
@@ -0,0 +1,18 @@
+name: RelativeCI
+
+on:
+ workflow_run:
+ workflows: ["build"]
+ types:
+ - completed
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send bundle stats and build information to RelativeCI (editor)
+ uses: relative-ci/agent-action@v2
+ with:
+ artifactName: relative-ci-artifacts-editor
+ key: ${{ secrets.RELATIVE_CI_KEY }}
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index ef4e11d35d..898dcdcc62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,7 @@ packages/*/types
examples/*/types
/.pnp
.pnp.js
-
+tsconfig.tsbuildinfo
# testing
coverage
@@ -29,4 +29,15 @@ yarn-error.log*
.vercel
test-results/
playwright-report/
-release
\ No newline at end of file
+blob-report/
+release
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+.env
+*.pem
+.nx/
+# Nightshift plan artifacts (keep out of version control)
+.nightshift-plan
+.claude
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index b1215e8764..c5ddcef4e7 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v18.16.0
\ No newline at end of file
+v22.14.0
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000..0f4e5b7d67
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json.schemastore.org/prettierrc",
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "printWidth": 80,
+ "trailingComma": "all",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "endOfLine": "lf",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/.resources/release-workflow.excalidraw.svg b/.resources/release-workflow.excalidraw.svg
new file mode 100644
index 0000000000..635f94e031
--- /dev/null
+++ b/.resources/release-workflow.excalidraw.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f46e5ae865..7c24081186 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,12 +2,24 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
- "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"search.exclude": {
- "packages/editor/public/types": true
+ "packages/editor/public/types": true,
+ "packages/website/docs/.vitepress": false,
+ "**/.*": false
},
- "typescript.tsdk": "node_modules/typescript/lib"
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "[xml]": {
+ "editor.defaultFormatter": "redhat.vscode-xml"
+ },
+ "scm.defaultViewMode": "tree",
+ "search.defaultViewMode": "tree",
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[mdx]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..112608f8ff
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,862 @@
+## 0.51.0 (2026-05-14)
+
+### 🚀 Features
+
+- Trailing block extension rewrite ([#2733](https://github.com/TypeCellOS/BlockNote/pull/2733))
+- **markdown:** replace unified.js with custom markdown parser/serializer ([#2624](https://github.com/TypeCellOS/BlockNote/pull/2624))
+- **react:** configurable portal targets for floating UI ([#2729](https://github.com/TypeCellOS/BlockNote/pull/2729), [#2692](https://github.com/TypeCellOS/BlockNote/issues/2692))
+
+### 🩹 Fixes
+
+- Pasting plain text from VSCode (BLO-366) ([#2713](https://github.com/TypeCellOS/BlockNote/pull/2713))
+- Parse new lines in `text/plain` as line breaks (BLO-1170) ([#2712](https://github.com/TypeCellOS/BlockNote/pull/2712))
+- Code block PDF export (BLO-987) ([#2725](https://github.com/TypeCellOS/BlockNote/pull/2725))
+- Formatting toolbar opening when inserting file block with `trailingBlock: false` (BLO-860) ([#2704](https://github.com/TypeCellOS/BlockNote/pull/2704))
+- numbered list item decorations missed on initial render ([#2734](https://github.com/TypeCellOS/BlockNote/pull/2734))
+- flicker-free mobile formatting toolbar via CSS custom properties ([#2617](https://github.com/TypeCellOS/BlockNote/pull/2617), [#2616](https://github.com/TypeCellOS/BlockNote/issues/2616))
+- add `bn-thread-orphaned` CSS class to distinguish orphaned threads ([#2737](https://github.com/TypeCellOS/BlockNote/pull/2737), [#2735](https://github.com/TypeCellOS/BlockNote/issues/2735))
+- set width attribute on image and video elements in editor render ([#2740](https://github.com/TypeCellOS/BlockNote/pull/2740), [#2726](https://github.com/TypeCellOS/BlockNote/issues/2726))
+- **a11y:** use figure/figcaption for media block captions ([#2717](https://github.com/TypeCellOS/BlockNote/pull/2717))
+- **ai:** loosen serialization of blocks in columns ([#2716](https://github.com/TypeCellOS/BlockNote/pull/2716), [#2718](https://github.com/TypeCellOS/BlockNote/pull/2718))
+- **core:** trigger codeblock input rule on Enter and place cursor inside ([#2686](https://github.com/TypeCellOS/BlockNote/pull/2686))
+- **core:** preserve list item type when pasting into empty list items ([#2722](https://github.com/TypeCellOS/BlockNote/pull/2722), [#2330](https://github.com/TypeCellOS/BlockNote/issues/2330))
+- **core:** unmount editors in transformPasted tests to prevent unhandled error ([e62880b21](https://github.com/TypeCellOS/BlockNote/commit/e62880b21))
+- **drag-n-drop:** support PDF block drag & drop (BLO-893) ([#2714](https://github.com/TypeCellOS/BlockNote/pull/2714))
+- **i18:** improve french translation for empty toggle list ([#2721](https://github.com/TypeCellOS/BlockNote/pull/2721))
+- **markdown:** emit tight lists when serializing blocks to markdown ([#2715](https://github.com/TypeCellOS/BlockNote/pull/2715))
+- **markdown:** skip placeholder text for empty files ([#434](https://github.com/TypeCellOS/BlockNote/pull/434), [#2719](https://github.com/TypeCellOS/BlockNote/pull/2719))
+- **markdown:** stable round-trip for tables, captions, and audio ([#2720](https://github.com/TypeCellOS/BlockNote/pull/2720))
+- **tests:** stabilize webkit keyboard handler tests with programmatic cursor positioning ([#2746](https://github.com/TypeCellOS/BlockNote/pull/2746))
+
+### ❤️ Thank You
+
+- Cyril G
+- Manuel Raynaud @lunika
+- Matthew Lipski @matthewlipski
+- Movm
+- Nick Perez
+- Nick the Sick @nperez0111
+
+## 0.50.0 (2026-05-04)
+
+### 🚀 Features
+
+- Dark mode styling for file block wrapper component (BLO-866) ([#2680](https://github.com/TypeCellOS/BlockNote/pull/2680))
+- Drag hendle menu delete button removes all other blocks in selection (BLO-1007) ([#2683](https://github.com/TypeCellOS/BlockNote/pull/2683))
+- Enter moves selection to cell below in tables (BLO-1006) ([#2685](https://github.com/TypeCellOS/BlockNote/pull/2685))
+- additional heading top padding (BLO-1008) ([#2690](https://github.com/TypeCellOS/BlockNote/pull/2690))
+- Code mark input rule edge case (BLO-938) ([#2698](https://github.com/TypeCellOS/BlockNote/pull/2698))
+- **mantine:** upgrade @mantine/core and @mantine/hooks to v9.0.2 ([#2655](https://github.com/TypeCellOS/BlockNote/pull/2655))
+
+### 🩹 Fixes
+
+- Hardcoded strings in comment components (BLO-1033) ([#2681](https://github.com/TypeCellOS/BlockNote/pull/2681))
+- Color naming & CSS (BLO-946) ([#2684](https://github.com/TypeCellOS/BlockNote/pull/2684))
+- link HTML attributes (BLO-915) ([#2687](https://github.com/TypeCellOS/BlockNote/pull/2687))
+- guard hideMenuIfNotFrozen against undefined view state ([#2694](https://github.com/TypeCellOS/BlockNote/pull/2694), [#2699](https://github.com/TypeCellOS/BlockNote/pull/2699))
+- Clicking comment overlapping link opens link (BLO-1091) ([#2696](https://github.com/TypeCellOS/BlockNote/pull/2696))
+- prevent table row drag from moving an extra adjacent row ([#2703](https://github.com/TypeCellOS/BlockNote/pull/2703))
+- **clipboard:** use ProseMirror selection state for Shadow DOM compatibility ([#2677](https://github.com/TypeCellOS/BlockNote/pull/2677))
+
+### ❤️ Thank You
+
+- jt_fox @LimChaeJune
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Wieland Lindenthal
+- Yousef
+
+## 0.49.0 (2026-04-24)
+
+### 🚀 Features
+
+- simplify links by inlining it to BlockNote ([#2623](https://github.com/TypeCellOS/BlockNote/pull/2623))
+- add Unicode quotation mark input rule for quote blocks ([#2673](https://github.com/TypeCellOS/BlockNote/pull/2673))
+
+### 🩹 Fixes
+
+- Inserting link removes comment & add comment button click buggy ([#2620](https://github.com/TypeCellOS/BlockNote/pull/2620), [#2573](https://github.com/TypeCellOS/BlockNote/issues/2573))
+- `useEditorDOMElement` hook ([#2619](https://github.com/TypeCellOS/BlockNote/pull/2619))
+- text color was not applying to table block ([#2663](https://github.com/TypeCellOS/BlockNote/pull/2663))
+- Drag preview blocking drops when overlapping the editor (BLO-996) ([#2670](https://github.com/TypeCellOS/BlockNote/pull/2670))
+- Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116) ([#2628](https://github.com/TypeCellOS/BlockNote/pull/2628), [#2603](https://github.com/TypeCellOS/BlockNote/issues/2603))
+- save file caption/name on every keystroke instead of on close ([#2575](https://github.com/TypeCellOS/BlockNote/pull/2575))
+- prevent FloatingFocusManager from resetting editor selection ([#2525](https://github.com/TypeCellOS/BlockNote/pull/2525), [#2664](https://github.com/TypeCellOS/BlockNote/pull/2664))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- miadnguyen @miadnguyen
+- mianguyen
+- Nick Perez
+- Yousef
+
+## 0.48.1 (2026-04-16)
+
+### 🩹 Fixes
+
+- make CustomChange compatible with prosemirror-changeset 2.4.1 ([#2647](https://github.com/TypeCellOS/BlockNote/pull/2647))
+- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability (CVE-2025-62718) ([c1ef3018a](https://github.com/TypeCellOS/BlockNote/commit/c1ef3018a))
+- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability ([#2653](https://github.com/TypeCellOS/BlockNote/pull/2653))
+- **docx-exporter:** omit w:lang when no locale provided instead of defaulting to en-US ([#2651](https://github.com/TypeCellOS/BlockNote/pull/2651))
+
+### ❤️ Thank You
+
+- Claude Opus 4.6 (1M context)
+- Nick Perez
+- Nick the Sick
+- Stephan Meijer @StephanMeijer
+
+## 0.48.0 (2026-04-13)
+
+### 🚀 Features
+
+- upgrade shiki to v4 and prosemirror-highlight to v0.15.1 ([#2625](https://github.com/TypeCellOS/BlockNote/pull/2625))
+- upgrade nx to 22.6.4 and liveblocks to 3.17.0 ([#2627](https://github.com/TypeCellOS/BlockNote/pull/2627))
+
+### 🩹 Fixes
+
+- Image block selection clears on mouse leave in Safari ([#2613](https://github.com/TypeCellOS/BlockNote/pull/2613))
+- Backspace bug when current block is empty and previous block's last child is empty ([#2610](https://github.com/TypeCellOS/BlockNote/pull/2610))
+- allow using latest @tiptap/extension-link version ([1ae8de713](https://github.com/TypeCellOS/BlockNote/commit/1ae8de713))
+- restore depth guard in getParentBlockInfo to prevent RangeError (blo-1103) ([#2585](https://github.com/TypeCellOS/BlockNote/pull/2585))
+- pin better-auth to ~1.4.x to fix docs build ([bda30458a](https://github.com/TypeCellOS/BlockNote/commit/bda30458a))
+- hide side menu on scroll instead of overflow hacks ([#2630](https://github.com/TypeCellOS/BlockNote/pull/2630), [#2043](https://github.com/TypeCellOS/BlockNote/issues/2043))
+- disable default UI when no components context is found ([#2611](https://github.com/TypeCellOS/BlockNote/pull/2611))
+- add .js extension to fast-deep-equal ESM import ([#2641](https://github.com/TypeCellOS/BlockNote/pull/2641))
+- placeholder when overflowing now wraps ([#2291](https://github.com/TypeCellOS/BlockNote/pull/2291))
+- **core:** fix unnesting blocks with siblings (BLO-1017) ([#2601](https://github.com/TypeCellOS/BlockNote/pull/2601))
+- **core:** backspace mid-text next to columnList moves block BLO-1126 ([#2629](https://github.com/TypeCellOS/BlockNote/pull/2629))
+
+### 🔥 Performance
+
+- optimize plugin traversals for large documents BLO-1111 ([#2600](https://github.com/TypeCellOS/BlockNote/pull/2600))
+
+### ❤️ Thank You
+
+- Claude Opus 4.6
+- Claude Opus 4.6 (1M context)
+- hedi-ghodhbane @hedi-ghodhbane
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Nick the Sick @nperez0111
+- Yousef
+
+## 0.47.3 (2026-03-25)
+
+### 🩹 Fixes
+
+- **core:** preserve whitespace edge cases but collapse html formatting newlines (BLO-1065) ([#2551](https://github.com/TypeCellOS/BlockNote/pull/2551), [#2230](https://github.com/TypeCellOS/BlockNote/issues/2230))
+
+### ❤️ Thank You
+
+- Yousef
+
+## 0.47.2 (2026-03-20)
+
+### 🩹 Fixes
+
+- use / for toggle block HTML export ([#2524](https://github.com/TypeCellOS/BlockNote/pull/2524))
+- remove @hocuspocus/provider peer dependency by inlining tiptap comment types BLO-1064 ([#2564](https://github.com/TypeCellOS/BlockNote/pull/2564))
+- **core:** slash menu fails in custom blocks after space BLO-1036 ([#2553](https://github.com/TypeCellOS/BlockNote/pull/2553))
+- **i18n:** fix typo in russian translation ([#2560](https://github.com/TypeCellOS/BlockNote/pull/2560))
+
+### ❤️ Thank You
+
+- Claude Opus 4.6
+- Drone
+- Yousef
+
+## 0.47.1 (2026-03-02)
+
+### 🩹 Fixes
+
+- typeerror cannot read properties of undefined ([#2522](https://github.com/TypeCellOS/BlockNote/pull/2522))
+- handle more delete key cases ([#2126](https://github.com/TypeCellOS/BlockNote/pull/2126))
+- add delay for `data-active` in collab cursors ([#2383](https://github.com/TypeCellOS/BlockNote/pull/2383))
+- disable slash menu in table content #2408 ([#2504](https://github.com/TypeCellOS/BlockNote/pull/2504), [#2408](https://github.com/TypeCellOS/BlockNote/issues/2408))
+- **ai:** selections broken due to floating-ui focus manager ([#2527](https://github.com/TypeCellOS/BlockNote/pull/2527))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Yousef
+
+## 0.47.0 (2026-02-23)
+
+### 🚀 Features
+
+- update suggestion menu component ([#2397](https://github.com/TypeCellOS/BlockNote/pull/2397))
+- **i18n:** add Persian (fa) localization support ([#2447](https://github.com/TypeCellOS/BlockNote/pull/2447))
+- **i18n:** add Uzbek (uz) localization support ([#2506](https://github.com/TypeCellOS/BlockNote/pull/2506))
+
+### 🩹 Fixes
+
+- prevent nested bullet list icon rendering as emoji on iOS 18+ ([#2394](https://github.com/TypeCellOS/BlockNote/pull/2394), [#2399](https://github.com/TypeCellOS/BlockNote/pull/2399))
+- ignore drag & drop from unrelated events #1968 ([#2346](https://github.com/TypeCellOS/BlockNote/pull/2346), [#1968](https://github.com/TypeCellOS/BlockNote/issues/1968))
+- disable checkbox when editor is not editable #2406 ([#2448](https://github.com/TypeCellOS/BlockNote/pull/2448), [#2406](https://github.com/TypeCellOS/BlockNote/issues/2406))
+- Backspace/enter behaviour in empty block with children ([#2451](https://github.com/TypeCellOS/BlockNote/pull/2451))
+- handle pasting into table cells better, by collapsing their content to inline #2410 ([#2449](https://github.com/TypeCellOS/BlockNote/pull/2449), [#2410](https://github.com/TypeCellOS/BlockNote/issues/2410))
+- **accessibility:** ai combobox aria-activedescendant ([#2413](https://github.com/TypeCellOS/BlockNote/pull/2413))
+- **ai:** no more scrolling to top when opening AI menu ([#2503](https://github.com/TypeCellOS/BlockNote/pull/2503))
+- **docs:** unicode char not rendered in bug template ([f13e270be](https://github.com/TypeCellOS/BlockNote/commit/f13e270be))
+
+### ❤️ Thank You
+
+- Cyril G @Ovgodd
+- Dex Devlon @bxff
+- Matthew Lipski @matthewlipski
+- MDSAM05 @MDSAM05
+- Mohammad RAHMANI @Mrahmani71
+- Nick Perez
+- Ogabek @OgabekYuldoshev
+- Wouter Vroege
+- Yousef
+
+## 0.46.2 (2026-01-27)
+
+### 🩹 Fixes
+
+- deep merge floatingUIOptions using nested spread operators ([#2310](https://github.com/TypeCellOS/BlockNote/pull/2310))
+- Visual differences between live editor and rendered exported HTML ([#2348](https://github.com/TypeCellOS/BlockNote/pull/2348))
+- `BlockNoteViewEditor` mismatched editable value ([#2357](https://github.com/TypeCellOS/BlockNote/pull/2357))
+- add `font-synthesis` for italic & bold in fonts that don't have them specified #2325 ([#2354](https://github.com/TypeCellOS/BlockNote/pull/2354), [#2325](https://github.com/TypeCellOS/BlockNote/issues/2325))
+- disable code block language selector when editor is not editable ([#2351](https://github.com/TypeCellOS/BlockNote/pull/2351))
+- table handles would crash ([#2384](https://github.com/TypeCellOS/BlockNote/pull/2384))
+- update CreateLinkButton to be able to toggle popover visibility ([#2316](https://github.com/TypeCellOS/BlockNote/pull/2316), [#2313](https://github.com/TypeCellOS/BlockNote/issues/2313))
+- add context,nestingLevel to toExternalHTML ([#2373](https://github.com/TypeCellOS/BlockNote/pull/2373))
+- **ai:** re-enable flipping the AIMenu when there is not enough space #2245 ([#2247](https://github.com/TypeCellOS/BlockNote/pull/2247), [#2245](https://github.com/TypeCellOS/BlockNote/issues/2245))
+- **link-toolbar:** prevent Enter from submitting during IME composition ([#2361](https://github.com/TypeCellOS/BlockNote/pull/2361))
+
+### ❤️ Thank You
+
+- hanios123
+- Jean-Baptiste PENRATH
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Shohei Yoshida @ysds
+- Yousef
+
+## 0.46.1 (2026-01-10)
+
+This was a version bump only, there were no code changes.
+
+## 0.46.0 (2026-01-08)
+
+### 🚀 Features
+
+- add data-nesting-level to HTML export ([#2329](https://github.com/TypeCellOS/BlockNote/pull/2329))
+- migrate to ai sdk 6 ([#2328](https://github.com/TypeCellOS/BlockNote/pull/2328))
+
+### 🩹 Fixes
+
+- emojipicker can sometimes fail to mount ([575b81cec](https://github.com/TypeCellOS/BlockNote/commit/575b81cec))
+- LinkToolbar Event Listener leak ([#2335](https://github.com/TypeCellOS/BlockNote/pull/2335))
+- when you convert a block into checkListItem via inputRule, it should transfer its content into checkListItem content ([#2331](https://github.com/TypeCellOS/BlockNote/pull/2331))
+- do not return focus back to menu ([484d7da36](https://github.com/TypeCellOS/BlockNote/commit/484d7da36))
+- arrow up on a checklist item should move to the element above BLO-362 ([#2306](https://github.com/TypeCellOS/BlockNote/pull/2306))
+- getPos race condition in React StrictMode ([#2311](https://github.com/TypeCellOS/BlockNote/pull/2311))
+- adjust input rules to be more tolerant to starting whitespace ([#2341](https://github.com/TypeCellOS/BlockNote/pull/2341))
+- **ai:** make sure ShowSelection works ([#2297](https://github.com/TypeCellOS/BlockNote/pull/2297))
+- **xl-email-exporter:** remove redundant sections in email export ([#2323](https://github.com/TypeCellOS/BlockNote/pull/2323))
+
+### ❤️ Thank You
+
+- Nick Perez
+- Nick the Sick @nperez0111
+- supernova @tmpluto
+- Yousef
+
+## 0.45.0 (2025-12-17)
+
+### 🚀 Features
+
+- **ai:** expand selections to contain words ([#2304](https://github.com/TypeCellOS/BlockNote/pull/2304))
+- **extensions:** extensions can now include other extensions for grouping into one extension ([#2284](https://github.com/TypeCellOS/BlockNote/pull/2284))
+
+### 🩹 Fixes
+
+- an invalidly specified table should not crash the editor ([#2255](https://github.com/TypeCellOS/BlockNote/pull/2255))
+- filter out invalid heading items based on the current block schema in the slash menu #2253 ([#2259](https://github.com/TypeCellOS/BlockNote/pull/2259), [#2253](https://github.com/TypeCellOS/BlockNote/issues/2253))
+- relax shiki package requirements #2279 ([#2280](https://github.com/TypeCellOS/BlockNote/pull/2280), [#2279](https://github.com/TypeCellOS/BlockNote/issues/2279))
+- filter the default tiptap extensions #2282 ([#2283](https://github.com/TypeCellOS/BlockNote/pull/2283), [#2282](https://github.com/TypeCellOS/BlockNote/issues/2282))
+- always include the cursor extension #2244 ([#2260](https://github.com/TypeCellOS/BlockNote/pull/2260), [#2244](https://github.com/TypeCellOS/BlockNote/issues/2244))
+- make `onBeforeChange` return the correct type again ([9009369b1](https://github.com/TypeCellOS/BlockNote/commit/9009369b1))
+- if there is no table block, there is no table handles to show #1055 ([#2281](https://github.com/TypeCellOS/BlockNote/pull/2281), [#1055](https://github.com/TypeCellOS/BlockNote/issues/1055))
+- pass dragHandleMenu prop to DragHandleButton ([#2254](https://github.com/TypeCellOS/BlockNote/pull/2254))
+- html diff error with whitespace ([#2230](https://github.com/TypeCellOS/BlockNote/pull/2230))
+- update regex for checklist items #2288 ([#2305](https://github.com/TypeCellOS/BlockNote/pull/2305), [#2288](https://github.com/TypeCellOS/BlockNote/issues/2288))
+- **email-exporter:** ReadableByteStreamController for safari react-email ([#2295](https://github.com/TypeCellOS/BlockNote/pull/2295))
+
+### ❤️ Thank You
+
+- Max @maqen
+- Nick Perez
+- Nick the Sick @nperez0111
+- Yousef
+
+## 0.44.2 (2025-12-09)
+
+### 🩹 Fixes
+
+- put back `onBeforeChange` method #2221 ([#2243](https://github.com/TypeCellOS/BlockNote/pull/2243), [#2221](https://github.com/TypeCellOS/BlockNote/issues/2221))
+- Improper accessing of editor DOM element ([#2234](https://github.com/TypeCellOS/BlockNote/pull/2234))
+- make validation errors recoverable by llm ([#2054](https://github.com/TypeCellOS/BlockNote/pull/2054))
+- shadowdom support and example ([#2223](https://github.com/TypeCellOS/BlockNote/pull/2223))
+- ensure numbered list start property always present ([#2241](https://github.com/TypeCellOS/BlockNote/pull/2241), [#2242](https://github.com/TypeCellOS/BlockNote/pull/2242))
+- Suggestion menu positioning ([#2232](https://github.com/TypeCellOS/BlockNote/pull/2232))
+- conditionally access the TableHandles extension from React ([#2248](https://github.com/TypeCellOS/BlockNote/pull/2248))
+- **ai:** upgrade prosemirror-suggest-changes ([#2235](https://github.com/TypeCellOS/BlockNote/pull/2235))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- wcyat @sdip15fa
+- Yousef
+
+## 0.44.1 (2025-12-08)
+
+### 🩹 Fixes
+
+- clearing selection was not being called when create link button is no longer rendered ([#2217](https://github.com/TypeCellOS/BlockNote/pull/2217))
+- AI menu not updating position on new line ([#2233](https://github.com/TypeCellOS/BlockNote/pull/2233))
+- UI elements not scrolling when editor DOM element is scrollable ([#2231](https://github.com/TypeCellOS/BlockNote/pull/2231))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+
+## 0.44.0 (2025-12-02)
+
+### 🚀 Features
+
+- **ai:** Abort requests ([#2213](https://github.com/TypeCellOS/BlockNote/pull/2213))
+
+### ❤️ Thank You
+
+- Yousef
+
+## 0.43.0 (2025-12-01)
+
+### 🚀 Features
+
+- Major Extensions & UI Refactor ([#2143](https://github.com/TypeCellOS/BlockNote/pull/2143))
+
+### 🩹 Fixes
+
+- allow configuring the email body's styles ([#2182](https://github.com/TypeCellOS/BlockNote/pull/2182))
+- **xl-docx-exporter:** improve OOXML interoperability ([#2206](https://github.com/TypeCellOS/BlockNote/pull/2206))
+
+### ❤️ Thank You
+
+- Nick Perez
+- Stephan Meijer @StephanMeijer
+
+## 0.42.3 (2025-11-19)
+
+### 🩹 Fixes
+
+- disallow access to the `domElement` or `isFocused` if the editor is unmounted ([#2187](https://github.com/TypeCellOS/BlockNote/pull/2187))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.42.2 (2025-11-19)
+
+### 🩹 Fixes
+
+- put back mounting system ([#2183](https://github.com/TypeCellOS/BlockNote/pull/2183))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.42.1 (2025-11-18)
+
+### 🩹 Fixes
+
+- do not error on invalid `backgroundColor` or `textColor` #2176 ([#2179](https://github.com/TypeCellOS/BlockNote/pull/2179), [#2176](https://github.com/TypeCellOS/BlockNote/issues/2176))
+- remove dependency array from comments re-rendering ([#2177](https://github.com/TypeCellOS/BlockNote/pull/2177))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.42.0 (2025-11-11)
+
+### 🚀 Features
+
+- **yjs:** expose Y.js BlockNote conversion primitives #1866 ([#2166](https://github.com/TypeCellOS/BlockNote/pull/2166), [#1866](https://github.com/TypeCellOS/BlockNote/issues/1866))
+
+### 🩹 Fixes
+
+- Emoji picker issues ([#2092](https://github.com/TypeCellOS/BlockNote/pull/2092))
+- set a default for `blocksToFullHTML` #2100 ([#2101](https://github.com/TypeCellOS/BlockNote/pull/2101), [#2100](https://github.com/TypeCellOS/BlockNote/issues/2100))
+- correctly index blocks that have children fixes #2115 ([#2116](https://github.com/TypeCellOS/BlockNote/pull/2116), [#2115](https://github.com/TypeCellOS/BlockNote/issues/2115))
+- add more lenient parsing for code blocks, to accept newlines #2105 ([#2108](https://github.com/TypeCellOS/BlockNote/pull/2108), [#2105](https://github.com/TypeCellOS/BlockNote/issues/2105))
+- Firefox invisible text cursor after dropping blocks ([#2128](https://github.com/TypeCellOS/BlockNote/pull/2128))
+- parsing `priority` for custom inline content and styles ([#2119](https://github.com/TypeCellOS/BlockNote/pull/2119))
+- `BlockTypeSelect` item filtering based on schema ([#2112](https://github.com/TypeCellOS/BlockNote/pull/2112))
+- deleting last block in column ([#2110](https://github.com/TypeCellOS/BlockNote/pull/2110))
+- **comments:** update the styles for the cursor to be the default cursor ([#2163](https://github.com/TypeCellOS/BlockNote/pull/2163))
+- **comments:** always surface the closest mark to the current position ([#2164](https://github.com/TypeCellOS/BlockNote/pull/2164))
+- **comments:** scrolling bug when clicking comment marks ([#2165](https://github.com/TypeCellOS/BlockNote/pull/2165))
+- **react:** destroy editor instances after two ticks ([#2121](https://github.com/TypeCellOS/BlockNote/pull/2121))
+- **schema-migration:** more robust migration of background-color & text-color attributes ([#2154](https://github.com/TypeCellOS/BlockNote/pull/2154))
+- **unique-id:** do not attempt to append to y-sync plugin transactions ([#2153](https://github.com/TypeCellOS/BlockNote/pull/2153))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+
+## 0.41.1 (2025-10-09)
+
+This was a version bump only, there were no code changes.
+
+## 0.41.0 (2025-10-08)
+
+### 🚀 Features
+
+- AI menu auto scrolling ([#2039](https://github.com/TypeCellOS/BlockNote/pull/2039))
+- Shortcut to delete empty table while cells are selected ([#2052](https://github.com/TypeCellOS/BlockNote/pull/2052))
+- **divider:** add a divider block ([#2014](https://github.com/TypeCellOS/BlockNote/pull/2014))
+
+### 🩹 Fixes
+
+- Code block language select value not updating properly ([#2050](https://github.com/TypeCellOS/BlockNote/pull/2050))
+- disable input rules for numbered headings #1789 ([#2032](https://github.com/TypeCellOS/BlockNote/pull/2032), [#1789](https://github.com/TypeCellOS/BlockNote/issues/1789))
+- video parsing and export for markdown ([#1955](https://github.com/TypeCellOS/BlockNote/pull/1955))
+- Reaction picker shown for users who can't react ([#2061](https://github.com/TypeCellOS/BlockNote/pull/2061))
+- Add Mantine dependency to individual examples ([#2070](https://github.com/TypeCellOS/BlockNote/pull/2070))
+- allow listening to `onChange` and other events before the underlying editor is initialized ([#2063](https://github.com/TypeCellOS/BlockNote/pull/2063))
+- toggle and check list item blocks ([#2071](https://github.com/TypeCellOS/BlockNote/pull/2071))
+- added missing fields to implementations in editor schema block specs ([#2046](https://github.com/TypeCellOS/BlockNote/pull/2046))
+
+### ❤️ Thank You
+
+- Héctor Zhuang @Hector-Zhuang
+- Matthew Lipski @matthewlipski
+- Nick Perez
+
+## 0.40.0 (2025-09-30)
+
+### 🚀 Features
+
+- Mantine v8 upgrade ([#2028](https://github.com/TypeCellOS/BlockNote/pull/2028), [#2029](https://github.com/TypeCellOS/BlockNote/issues/2029))
+- Update Mantine setup ([#2033](https://github.com/TypeCellOS/BlockNote/pull/2033))
+- **ai:** SDK 5, tool calling, custom backends ([#2007](https://github.com/TypeCellOS/BlockNote/pull/2007))
+- **core:** add the ability to autofocus on the editor element ([#2018](https://github.com/TypeCellOS/BlockNote/pull/2018))
+
+### 🩹 Fixes
+
+- Block colors menu not always showing ([#2027](https://github.com/TypeCellOS/BlockNote/pull/2027))
+- Update remianing examples to Mantine v8 ([#2031](https://github.com/TypeCellOS/BlockNote/pull/2031))
+- ShadCN example Tailwind setup ([#2042](https://github.com/TypeCellOS/BlockNote/pull/2042))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Yousef
+
+## 0.39.1 (2025-09-19)
+
+### 🩹 Fixes
+
+- cleanup accesses to prosemirrorView to account for tiptap 3 behavior ([#2017](https://github.com/TypeCellOS/BlockNote/pull/2017))
+- **core:** input rules can handle when a new block is empty now ([#2013](https://github.com/TypeCellOS/BlockNote/pull/2013))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.39.0 (2025-09-18)
+
+### 🚀 Features
+
+- move all blocks to use the custom blocks API ([#1904](https://github.com/TypeCellOS/BlockNote/pull/1904))
+- **core:** support for Tiptap V3 ([#2001](https://github.com/TypeCellOS/BlockNote/pull/2001))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.38.0 (2025-09-16)
+
+### 🚀 Features
+
+- Custom schemas for comment editors ([#1976](https://github.com/TypeCellOS/BlockNote/pull/1976))
+
+### 🩹 Fixes
+
+- Suggestion menu positioning ([#1975](https://github.com/TypeCellOS/BlockNote/pull/1975))
+- doLLMRequest fails when deleting a non-existent block ([#1982](https://github.com/TypeCellOS/BlockNote/pull/1982))
+- file block resize handles not working with touch inputs ([#1981](https://github.com/TypeCellOS/BlockNote/pull/1981))
+- get pdf example working again ([a90ae4d58](https://github.com/TypeCellOS/BlockNote/commit/a90ae4d58))
+- better markdown & html paste, make methods synchronous ([#1957](https://github.com/TypeCellOS/BlockNote/pull/1957))
+- Improve setting text for custom file blocks ([#1984](https://github.com/TypeCellOS/BlockNote/pull/1984))
+- **react:** close link popover on submit in static formatting toolbar #1696 ([#1997](https://github.com/TypeCellOS/BlockNote/pull/1997), [#1696](https://github.com/TypeCellOS/BlockNote/issues/1696))
+
+### ❤️ Thank You
+
+- dsriva03 @dsriva03
+- Héctor Zhuang @Hector-Zhuang
+- Matthew Lipski @matthewlipski
+- Nick the Sick
+
+## 0.37.0 (2025-08-29)
+
+### 🚀 Features
+
+- export `ShadCNComponentsContext` ([#1965](https://github.com/TypeCellOS/BlockNote/pull/1965))
+
+### 🩹 Fixes
+
+- Typing in empty table cells ([#1973](https://github.com/TypeCellOS/BlockNote/pull/1973))
+
+### ❤️ Thank You
+
+- Héctor Zhuang @Hector-Zhuang
+- Matthew Lipski @matthewlipski
+
+## 0.36.1 (2025-08-27)
+
+### 🩹 Fixes
+
+- table column widths not being set in exported HTML ([#1947](https://github.com/TypeCellOS/BlockNote/pull/1947))
+- Minor change to formatting toolbar extension logic ([#1963](https://github.com/TypeCellOS/BlockNote/pull/1963))
+- **core:** report block moves in `getBlocksChangedByTransaction` #1924 ([#1960](https://github.com/TypeCellOS/BlockNote/pull/1960), [#1924](https://github.com/TypeCellOS/BlockNote/issues/1924))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+
+## 0.36.0 (2025-08-25)
+
+### 🚀 Features
+
+- **docx:** add locale configuration for docx export ([#1937](https://github.com/TypeCellOS/BlockNote/pull/1937))
+
+### 🩹 Fixes
+
+- Editors in comments not inheriting theme ([#1890](https://github.com/TypeCellOS/BlockNote/pull/1890))
+- Minor drag & drop changes ([#1891](https://github.com/TypeCellOS/BlockNote/pull/1891))
+- Overflow on table blocks ([#1892](https://github.com/TypeCellOS/BlockNote/pull/1892))
+- Suggestion menu closing when clicking scroll bar ([#1899](https://github.com/TypeCellOS/BlockNote/pull/1899))
+- Table padding ([#1906](https://github.com/TypeCellOS/BlockNote/pull/1906))
+- Formatting toolbar getting wrong bounding box when updating React inline content ([#1908](https://github.com/TypeCellOS/BlockNote/pull/1908))
+- Vanilla blocks return true for editor.isEditable on initial render ([#1925](https://github.com/TypeCellOS/BlockNote/pull/1925))
+- table cell menu styling ([#1945](https://github.com/TypeCellOS/BlockNote/pull/1945))
+- Missing internationalization for toggle wrapper ([#1946](https://github.com/TypeCellOS/BlockNote/pull/1946))
+- parse image alt text for image blocks ([#1883](https://github.com/TypeCellOS/BlockNote/pull/1883))
+- initialize esm deps before copy extension uses it ([#1951](https://github.com/TypeCellOS/BlockNote/pull/1951))
+- error when dragging a block from one editor to another with multiple column extension ([#1950](https://github.com/TypeCellOS/BlockNote/pull/1950))
+- prevent infinite render loop when selecting all content ([#1956](https://github.com/TypeCellOS/BlockNote/pull/1956))
+- **core:** maintain text selection across table updates ([#1894](https://github.com/TypeCellOS/BlockNote/pull/1894))
+- **locales:** ko locale fix ([#1902](https://github.com/TypeCellOS/BlockNote/pull/1902))
+- **react:** add data attribute for correct react rendering ([#1954](https://github.com/TypeCellOS/BlockNote/pull/1954))
+- **xl-email-exporter:** better defaults, customize textStyles, output inline styles ([#1856](https://github.com/TypeCellOS/BlockNote/pull/1856))
+
+### ❤️ Thank You
+
+- Brad Greenlee
+- Cyril G @Ovgodd
+- Héctor Zhuang @Hector-Zhuang
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Nick the Sick
+
+## 0.35.0 (2025-07-25)
+
+### 🚀 Features
+
+- use fumadocs for website ([#1654](https://github.com/TypeCellOS/BlockNote/pull/1654))
+- llms.mdx routes ([cea93840e](https://github.com/TypeCellOS/BlockNote/commit/cea93840e))
+
+### 🩹 Fixes
+
+- insert file upload before block if it is closer to the top of the block ([#1857](https://github.com/TypeCellOS/BlockNote/pull/1857))
+- rename albert model ([3b0ba8d25](https://github.com/TypeCellOS/BlockNote/commit/3b0ba8d25))
+- resolve some minor drag & drop regressions ([#1862](https://github.com/TypeCellOS/BlockNote/pull/1862))
+- blockquote HTML parsing #1762 ([#1877](https://github.com/TypeCellOS/BlockNote/pull/1877), [#1762](https://github.com/TypeCellOS/BlockNote/issues/1762))
+
+### ❤️ Thank You
+
+- Brad Greenlee
+- Nick Perez
+- Nick the Sick
+- yousefed
+
+## 0.34.0 (2025-07-17)
+
+### 🚀 Features
+
+- support multi-column block in PDF, DOCX & ODT exporters ([#1781](https://github.com/TypeCellOS/BlockNote/pull/1781))
+- support react 19 ([f7b3466d3](https://github.com/TypeCellOS/BlockNote/commit/f7b3466d3))
+- disable conversion of headings to list items ([#1799](https://github.com/TypeCellOS/BlockNote/pull/1799))
+- report `moves` (indents and outdents) as changes when using `getChanges` #1757 ([#1786](https://github.com/TypeCellOS/BlockNote/pull/1786), [#1757](https://github.com/TypeCellOS/BlockNote/issues/1757))
+- allow inline content to be `draggable` ([#1818](https://github.com/TypeCellOS/BlockNote/pull/1818))
+- added type guards, types, and `editor` prop to custom inline content rendering ([#1736](https://github.com/TypeCellOS/BlockNote/pull/1736))
+- **block-change:** adds a new API for blocking changes to editor state, by filtering transactions ([#1750](https://github.com/TypeCellOS/BlockNote/pull/1750))
+
+### 🩹 Fixes
+
+- remove lookbehind regex for browser compat ([#1827](https://github.com/TypeCellOS/BlockNote/pull/1827))
+- `ToggleWrapper` button defaulting to `submit` type ([#1823](https://github.com/TypeCellOS/BlockNote/pull/1823))
+- disable $ref in AI schemas (html format) ([#1819](https://github.com/TypeCellOS/BlockNote/pull/1819))
+- re-evaluate side-menu on scroll ([#1830](https://github.com/TypeCellOS/BlockNote/pull/1830))
+- hide table extend buttons when not editable #1848 ([#1850](https://github.com/TypeCellOS/BlockNote/pull/1850), [#1848](https://github.com/TypeCellOS/BlockNote/issues/1848))
+- resolve several drag & drop issues ([#1845](https://github.com/TypeCellOS/BlockNote/pull/1845))
+
+### ❤️ Thank You
+
+- Arek Nawo @areknawo
+- Gonçalo Basto @gbasto
+- Héctor Zhuang @Hector-Zhuang
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Nick the Sick @nperez0111
+- Yousef
+
+## 0.33.0 (2025-07-03)
+
+### 🚀 Features
+
+- FloatingUI options prop for `BlockPositioner` ([#1801](https://github.com/TypeCellOS/BlockNote/pull/1801))
+- Support Google Gemini AI ([#1805](https://github.com/TypeCellOS/BlockNote/pull/1805))
+
+### 🩹 Fixes
+
+- support multi-character suggestions ([#1734](https://github.com/TypeCellOS/BlockNote/pull/1734))
+- switch foreground color based on selected user color dynamically #1785 ([#1787](https://github.com/TypeCellOS/BlockNote/pull/1787), [#1785](https://github.com/TypeCellOS/BlockNote/issues/1785))
+- mark react package as external in email exporter ([#1807](https://github.com/TypeCellOS/BlockNote/pull/1807))
+- Duplicate `formatConversionTest` files ([#1798](https://github.com/TypeCellOS/BlockNote/pull/1798))
+- AI empty document handling ([#1810](https://github.com/TypeCellOS/BlockNote/pull/1810))
+- `bn-inline-content` class name getting duplicated ([#1794](https://github.com/TypeCellOS/BlockNote/pull/1794))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Yousef
+
+## 0.32.0 (2025-06-24)
+
+### 🚀 Features
+
+- toggle blocks ([#1707](https://github.com/TypeCellOS/BlockNote/pull/1707))
+- **core:** support h4, h5, and h6 ([#1634](https://github.com/TypeCellOS/BlockNote/pull/1634))
+- **xl-email-exporter:** add email exporter ([#1768](https://github.com/TypeCellOS/BlockNote/pull/1768))
+
+### 🩹 Fixes
+
+- react 19 strict mode compatibility ([#1726](https://github.com/TypeCellOS/BlockNote/pull/1726))
+- add keys to pdf exporter ([#1739](https://github.com/TypeCellOS/BlockNote/pull/1739))
+- only listten for left click on formatting toolbar ([#1774](https://github.com/TypeCellOS/BlockNote/pull/1774))
+- prevent formatting toolbar from closing if click was from inside the editor ([#1775](https://github.com/TypeCellOS/BlockNote/pull/1775))
+- **locales:** add Hebrew translations for various components ([#1779](https://github.com/TypeCellOS/BlockNote/pull/1779))
+
+### ❤️ Thank You
+
+- Aslam @Aslam97
+- Drew Johnson
+- Jonathan Marbutt @jmarbutt
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Samuel Bisberg
+- Yousef
+
+## 0.31.3 (2025-06-18)
+
+### 🩹 Fixes
+
+- AI generation with empty document ([#1740](https://github.com/TypeCellOS/BlockNote/pull/1740))
+- do not send a welcome email if magic link was used on an account older than a minute ago ([db88fe4aa](https://github.com/TypeCellOS/BlockNote/commit/db88fe4aa))
+- AI system messages should always be at start of prompt ([#1741](https://github.com/TypeCellOS/BlockNote/pull/1741))
+- Selection clicking editor padding ([#1717](https://github.com/TypeCellOS/BlockNote/pull/1717))
+- preserve marks across a shift+enter #1672 ([#1743](https://github.com/TypeCellOS/BlockNote/pull/1743), [#1672](https://github.com/TypeCellOS/BlockNote/issues/1672))
+- **ai:** undo-redo after accepting/rejecting changes will undo as expected ([#1752](https://github.com/TypeCellOS/BlockNote/pull/1752))
+- **locales:** add translations for some comment strings ([#1764](https://github.com/TypeCellOS/BlockNote/pull/1764))
+- **website:** log in bug fixes ([#1742](https://github.com/TypeCellOS/BlockNote/pull/1742))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Nick the Sick
+- Vinicius Fernandes @ViniCleFer
+- Yousef
+
+## 0.31.2 (2025-06-05)
+
+### 🩹 Fixes
+
+- re-release ([0bc546e18](https://github.com/TypeCellOS/BlockNote/commit/0bc546e18))
+- ignore falsy values in boolean prop schema ([#1730](https://github.com/TypeCellOS/BlockNote/pull/1730))
+
+### ❤️ Thank You
+
+- Nick Perez
+- Nick the Sick
+
+## 0.31.1 (2025-05-23)
+
+### 🩹 Fixes
+
+- backwards-compat for `_extensions` ([#1708](https://github.com/TypeCellOS/BlockNote/pull/1708))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.31.0 (2025-05-20)
+
+### 🩹 Fixes
+
+- Playwright flaky keyboard handler test ([#1704](https://github.com/TypeCellOS/BlockNote/pull/1704))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+
+## 0.30.1 (2025-05-20)
+
+### 🩹 Fixes
+
+- better type-safety ([678086d4d](https://github.com/TypeCellOS/BlockNote/commit/678086d4d))
+- do not use `editor.dispatch` ([#1698](https://github.com/TypeCellOS/BlockNote/pull/1698))
+- re-added `display: flex` to blocks without inline content ([#1702](https://github.com/TypeCellOS/BlockNote/pull/1702))
+- **react:** add missing exports ([#1689](https://github.com/TypeCellOS/BlockNote/pull/1689))
+
+### ❤️ Thank You
+
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Nick the Sick
+
+## 0.30.0 (2025-05-09)
+
+### 🚀 Features
+
+- expose `editor.prosemirrorState` again ([#1615](https://github.com/TypeCellOS/BlockNote/pull/1615))
+- add `undo` and `redo` methods to editor API ([#1592](https://github.com/TypeCellOS/BlockNote/pull/1592))
+- new auth & payment system ([#1617](https://github.com/TypeCellOS/BlockNote/pull/1617))
+- re-implement Y.js collaboration as BlockNote plugins ([#1638](https://github.com/TypeCellOS/BlockNote/pull/1638))
+- **file:** `previewWidth` prop now defaults to `undefined` ([#1664](https://github.com/TypeCellOS/BlockNote/pull/1664))
+- **locales:** add zh-TW i18n ([#1668](https://github.com/TypeCellOS/BlockNote/pull/1668))
+
+### 🩹 Fixes
+
+- Formatting toolbar regression ([#1630](https://github.com/TypeCellOS/BlockNote/pull/1630))
+- provide `blockId` to `uploadFile` in UploadTab ([#1641](https://github.com/TypeCellOS/BlockNote/pull/1641))
+- do not close the menu on content/selection change ([#1644](https://github.com/TypeCellOS/BlockNote/pull/1644))
+- keep file panel open during collaboration ([#1646](https://github.com/TypeCellOS/BlockNote/pull/1646))
+- force pasting plain text into code block ([#1663](https://github.com/TypeCellOS/BlockNote/pull/1663))
+- updating HTML parsing rules to account for `prosemirror-model@1.25.1` ([#1661](https://github.com/TypeCellOS/BlockNote/pull/1661))
+- **code-block:** handle unknown languages better ([#1626](https://github.com/TypeCellOS/BlockNote/pull/1626))
+- **locales:** add slovak i18n ([#1649](https://github.com/TypeCellOS/BlockNote/pull/1649))
+
+### ❤️ Thank You
+
+- l0st0 @l0st0
+- Lawrence Lin @linyiru
+- Matthew Lipski @matthewlipski
+- Nick Perez
+- Quentin Nativel
+
+## 0.29.1 (2025-04-17)
+
+### 🩹 Fixes
+
+- try not to always use workspace version ([7af344ea9](https://github.com/TypeCellOS/BlockNote/commit/7af344ea9))
+
+### ❤️ Thank You
+
+- Nick the Sick
+
+## 0.29.0 (2025-04-17)
+
+### 🚀 Features
+
+- `change` event allows getting a list of the block changed ([#1585](https://github.com/TypeCellOS/BlockNote/pull/1585))
+
+### 🩹 Fixes
+
+- allow opening another suggestion menu if another is triggered #1473 ([#1591](https://github.com/TypeCellOS/BlockNote/pull/1591), [#1473](https://github.com/TypeCellOS/BlockNote/issues/1473))
+- add quote to schema ([aa16b15fe](https://github.com/TypeCellOS/BlockNote/commit/aa16b15fe))
+- update y-prosemirror to fix #1462 ([#1608](https://github.com/TypeCellOS/BlockNote/pull/1608), [#1462](https://github.com/TypeCellOS/BlockNote/issues/1462))
+- dispatch suggestion menu as a separate transaction ([#1614](https://github.com/TypeCellOS/BlockNote/pull/1614))
+
+### ❤️ Thank You
+
+- Nick Perez
+- Nick the Sick
+
+## 0.28.0 (2025-04-07)
+
+### 🚀 Features
+
+- position storage ([#1529](https://github.com/TypeCellOS/BlockNote/pull/1529))
+
+### ❤️ Thank You
+
+- Nick Perez
+
+## 0.27.2 (2025-04-05)
+
+### 🩹 Fixes
+
+- minor update for publishing ([c2820fdac](https://github.com/TypeCellOS/BlockNote/commit/c2820fdac))
+
+### ❤️ Thank You
+
+- Nick the Sick
+
+## 0.27.1 (2025-04-05)
+
+### 🚀 Features
+
+- **nx-cloud:** set up nx workspace ([#1586](https://github.com/TypeCellOS/BlockNote/pull/1586))
+
+### 🩹 Fixes
+
+- update packages to use correct react versions ([ea11ebce0](https://github.com/TypeCellOS/BlockNote/commit/ea11ebce0))
+
+### ❤️ Thank You
+
+- Nick Perez
+- Nick the Sick
+
+## 0.27.0 (2025-04-04)
+
+### 🚀 Features
+
+- split out localization files for optimized bundle ([#1533](https://github.com/TypeCellOS/BlockNote/pull/1533))
+- remove shiki dep, add new @blocknote/code-block package for slim shiki build ([#1519](https://github.com/TypeCellOS/BlockNote/pull/1519))
+- Block quote ([#1563](https://github.com/TypeCellOS/BlockNote/pull/1563))
+- markdown pasting & custom paste handlers ([#1490](https://github.com/TypeCellOS/BlockNote/pull/1490))
+
+### 🩹 Fixes
+
+- Backspace in empty block deletes previous block ([#1505](https://github.com/TypeCellOS/BlockNote/pull/1505))
+- Selection when clicking past end of inline content ([#1553](https://github.com/TypeCellOS/BlockNote/pull/1553))
+- better expose setting a draghandlemenu's items #1525 ([#1526](https://github.com/TypeCellOS/BlockNote/pull/1526), [#1525](https://github.com/TypeCellOS/BlockNote/issues/1525))
+- Multi-block links ([#1565](https://github.com/TypeCellOS/BlockNote/pull/1565))
+- Hard break keyboard shortcut not working in custom blocks ([#1554](https://github.com/TypeCellOS/BlockNote/pull/1554))
+- Overlapping marks in comments ([#1564](https://github.com/TypeCellOS/BlockNote/pull/1564))
+- some more sentry fixes ([#1577](https://github.com/TypeCellOS/BlockNote/pull/1577))
+
+### ❤️ Thank You
+
+- Martinrsts @Martinrsts
+- Matthew Lipski @matthewlipski
+- Nick Perez
+
+## Previous Versions
+
+See [Github Releases](https://github.com/TypeCellOS/BlockNote/releases) for previous versions.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ac57b6f132..8fe42686f0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,29 +3,85 @@
Directory structure:
```
-blocknote
-├── packages/core - The core of the editor
-├── packages/react - The main library for use in React apps
-├── examples/editor - Example React app that embeds the editor
-├── examples/vanilla - An advanced example if you don't want to use React or want to build your own UI components
-└── tests - Playwright end to end tests
+BlockNote
+├── packages/core - The core of the editor, which includes all logic to get the editor running in vanilla JS.
+├── packages/react - A React wrapper and UI for the editor. Requires additional components for the UI.
+├── packages/ariakit - UI components for the `react` package, made with Ariakit.
+├── packages/mantine - UI components for the `react` package, made with Mantine.
+├── packages/shadcn - UI components for the `react` package, made with Shadcn.
+├── packages/server-util - Utilities for converting BlockNote documents into static HTML for server-side rendering.
+├── packages/dev-scripts - A set of tools for converting example editor setups into components for the BlockNote website.
+├── examples - Example editor setups used for demos in the BlockNote website and playground.
+├── docs - Code for the BlockNote website.
+├── playground - A basic page where you can quickly test each of the example editor setups.
+└── tests - Playwright end to end tests.
```
-An introduction into the BlockNote Prosemirror schema can be found in [packages/core/ARCHITECTURE.md](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/ARCHITECTURE.md).
+An introduction into the BlockNote Prosemirror schema can be found in [packages/core/src/pm-nodes/README.md](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/pm-nodes/README.md).
## Running
To run the project, open the command line in the project's root directory and enter the following commands:
- # Install all required npm modules for lerna, and bootstrap lerna packages
- npm install
- npm run bootstrap
+```bash
+# Install all required npm modules
+pnpm install
- # Start the example project
- npm start
+# Start the example project
+pnpm start
+```
## Adding packages
- Add the dependency to the relevant `package.json` file (packages/xxx/package.json)
-- run `npm run install-new-packages`
-- Double check `package-lock.json` to make sure only the relevant packages have been affected
+- Double check `pnpm-lock.yaml` to make sure only the relevant packages have been affected
+
+## Packages
+
+| Package | Size | Version |
+| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [@blocknote/core](https://github.com/TypeCellOS/BlockNote/tree/main/packages/core) | | |
+| [@blocknote/react](https://github.com/TypeCellOS/BlockNote/tree/main/packages/react) | | |
+| [@blocknote/ariakit](https://github.com/TypeCellOS/BlockNote/tree/main/packages/ariakit) | | |
+| [@blocknote/mantine](https://github.com/TypeCellOS/BlockNote/tree/main/packages/mantine) | | |
+| [@blocknote/shadcn](https://github.com/TypeCellOS/BlockNote/tree/main/packages/shadcn) | | |
+| [@blocknote/server-util](https://github.com/TypeCellOS/BlockNote/tree/main/packages/server-util) | | |
+
+## Releasing
+
+This diagram illustrates the release workflow for the BlockNote monorepo.
+
+
+
+Essentially, when the maintainers have decided to release a new version of BlockNote, they will:
+
+ 1. Check that the `main` branch is in a releasable state:
+ - CI status of main branch is green
+ - Builds are passing
+ 2. Bump the package versions using the `pnpm run deploy` command. This command will:
+ 1. Based on semantic versioning, determine the next version number.
+ 2. Apply the new version number to all publishable packages within the monorepo.
+ 3. Generate a changelog for the new version.
+ 4. Commit the changes to the `main` branch.
+ 5. Create a new git tag for the new version.
+ 6. Push the changes to the `origin` remote.
+ 7. Create a new GitHub Release with the same name as the new version.
+ 8. Trigger a release workflow.
+
+The release workflow will:
+
+1. Checkout the `main` branch.
+2. Install the dependencies.
+3. Build the project.
+4. Login to npm.
+5. Publish the packages to npm.
+
+### Publishing a new package
+
+From time to time, you may need to publish a new package to npm. To do this, you cannot just deploy the package to npm, you need to:
+
+ 1. Run `nx release version --dry-run` and check that the version number is correct for the package.
+ - Once this is done, you can run `nx release version` to actually apply the version bump locally (staged to your local git repo).
+ 2. Run `nx release changelog --from --dry-run` and check that the changelog is correct for the package.
+ - Once this is done, you can run the same command without the `--dry-run` flag to actually apply the changelog, commit & push the changes to the `main` branch.
+ 3. The release workflow will automatically publish the package to npm.
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index a612ad9813..0000000000
--- a/LICENSE
+++ /dev/null
@@ -1,373 +0,0 @@
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000000..35a742ce7e
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,5 @@
+BlockNote is 100% Open Source Software.
+
+Source code in this repository is covered by the "Mozilla Public License Version 2.0" (MPL-2.0) license, except for the XL packages. The MPL-2.0 license allows you to use BlockNote in commercial (and closed-source) applications. If you make changes to the BlockNote source files, you're expected to publish these changes so the wider community can benefit as well.
+
+The XL packages (source code in the `packages/xl-*` directories and published in NPM as `@blocknote/xl-*`) are licensed under the "GNU General Public License Version 3" (GPL-3.0). Additionally, a commercial license is available. See our website (https://www.blocknotejs.org/pricing) for more information and the commercial license terms.
\ No newline at end of file
diff --git a/README.md b/README.md
index 195917fd6b..f391efb3f3 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,42 @@
# Live demo
-Play with the editor @ [https://blocknote-main.vercel.app/](https://blocknote-main.vercel.app/).
-
-(Source in [examples/editor](/examples/editor))
+See our homepage @ [https://www.blocknotejs.org](https://www.blocknotejs.org/) or browse the [examples](https://www.blocknotejs.org/examples).
# Example code (React)
[](https://badge.fury.io/js/%40blocknote%2Freact)
```typescript
-import { BlockNoteView, useBlockNote } from "@blocknote/react";
-import "@blocknote/core/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/core/fonts/inter.css";
+import "@blocknote/mantine/style.css";
function App() {
- const editor = useBlockNote({
- onEditorContentChange: (editor) => {
- // Log the document to console on every update
- console.log(editor.getJSON());
- },
- });
+ const editor = useCreateBlockNote();
return ;
}
@@ -51,8 +44,6 @@ function App() {
`@blocknote/react` comes with a fully styled UI that makes it an instant, polished editor ready to use in your app.
-If you prefer to create your own UI components (menus), or don't want to use React, you can use `@blocknote/core` (_advanced_, [see docs](https://www.blocknotejs.org/docs/vanilla-js)).
-
# Features
BlockNote comes with a number of features and components to make it easy to embed a high-quality block-based editor in your app:
@@ -87,24 +78,19 @@ BlockNote comes with a number of features and components to make it easy to embe
# Feedback 🙋♂️🙋♀️
-We'd love to hear your thoughts and see your experiments, so [come and say hi on Discord](https://discord.gg/Qc2QTTH5dF) or [Matrix](https://matrix.to/#/#typecell-space:matrix.org).
+We'd love to hear your thoughts and see your experiments, so [come and say hi on Discord](https://discord.gg/Qc2QTTH5dF).
# Contributing 🙌
-See [CONTRIBUTING.md](CONTRIBUTING.md) for more info and guidance on how to run the project (TLDR: just use `npm start`).
+See [CONTRIBUTING.md](CONTRIBUTING.md) for more info and guidance on how to run the project (TLDR: just use `pnpm start`).
-Directory structure:
+The codebase is automatically tested using Vitest and Playwright.
-```
-blocknote
-├── packages/core - The core of the editor
-├── packages/react - The main library for use in React apps
-├── examples/editor - Example React app that embeds the editor
-├── examples/vanilla - An advanced example if you don't want to use React or want to build your own UI components
-└── tests - Playwright end to end tests
-```
+# License 📃
-The codebase is automatically tested using Vitest and Playwright.
+BlockNote is 100% Open Source Software. The majority of BlockNote is licensed under the [MPL-2.0 license](LICENSE-MPL.txt), which allows you to use BlockNote in commercial (and closed-source) applications. If you make changes to the BlockNote source files, you're expected to publish these changes so the wider community can benefit as well. [Learn more](https://fossa.com/blog/open-source-software-licenses-101-mozilla-public-license-2-0/).
+
+The XL packages (source code in the `packages/xl-*` directories and published in NPM as `@blocknote/xl-*`) are licensed under the GPL-3.0. If you cannot comply with this license and want to use the XL libraries, you'll need a commercial license. Refer to [our website](https://www.blocknotejs.org/pricing) for more information.
# Credits ❤️
@@ -117,3 +103,5 @@ BlockNote is built as part of [TypeCell](https://www.typecell.org). TypeCell is
Hosting and deployments powered by Vercel:
+
+This project is tested with BrowserStack
diff --git a/docs/.env.local.example b/docs/.env.local.example
new file mode 100644
index 0000000000..a2dba36b4b
--- /dev/null
+++ b/docs/.env.local.example
@@ -0,0 +1,35 @@
+AUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.vercel.app/32
+
+# Better Auth Deployed URL
+BETTER_AUTH_URL=http://localhost:3000
+
+# ======= OPTIONAL =======
+
+# # Polar Sandbox is used in dev mode: https://sandbox.polar.sh/
+# # You may need to delete your user in their dashboard if you get a "cannot attach new external ID error"
+# POLAR_ACCESS_TOKEN=
+# POLAR_WEBHOOK_SECRET=
+
+# # In production, we use postgres
+# POSTGRES_URL=
+
+# # Email
+# SMTP_HOST=
+# SMTP_USER=
+# SMTP_PASS=
+# SMTP_PORT=
+# # Insecure if false, secure if any other value
+# SMTP_SECURE=false
+
+# # For GitHub Signin method
+# AUTH_GITHUB_ID=
+# AUTH_GITHUB_SECRET=
+
+# # The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
+# # It's used for authentication when uploading source maps.
+# SENTRY_AUTH_TOKEN=
+
+NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY=
+NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL=
+
+TURNSTILE_SECRET_KEY=
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000000..c12953bff8
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,32 @@
+# deps
+/node_modules
+
+# generated content
+.source
+
+# test & build
+/coverage
+/.next/
+/out/
+/build
+*.tsbuildinfo
+
+# misc
+.DS_Store
+*.pem
+/.pnp
+.pnp.js
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# others
+.env*.local
+.vercel
+next-env.d.ts
+# Sentry Config File
+.env.sentry-build-plugin
+
+/content/examples/*/*
+/components/example/generated/
+sqlite.db
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000000..b8e4860113
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,105 @@
+# Website Development
+
+This is the code for the [BlockNote documentation website](https://www.blocknotejs.org). If you're looking to work on BlockNote itself, check the [`packages`](/packages/) folder.
+
+To get started with development of the website, you can follow these steps:
+
+1. Initialize the DB
+
+If you haven't already, you can initialize the database with the following command:
+
+```bash
+cd docs && pnpm run init-db
+```
+
+This will initialize an SQLite database at `./docs/sqlite.db`.
+
+2. Setup environment variables
+
+Copy the `.env.example` file to `.env.local` and set the environment variables.
+
+```bash
+cp .env.example .env.local
+```
+
+If you want to test logging in, or payments see more information below [in the environment variables section](#environment-variables).
+
+3. Start the development server from within the `./docs` directory.
+
+```bash
+pnpm run dev
+```
+
+This will start the development server on port 3000.
+
+## Environment Variables
+
+### Logging in
+
+To test logging in, you can set the following environment variables:
+
+```bash
+AUTH_SECRET=test
+# Github OAuth optionally
+AUTH_GITHUB_ID=test
+AUTH_GITHUB_SECRET=test
+```
+
+Note: the GITHUB_ID and GITHUB_SECRET are optional, but if you want to test logging in with Github you'll need to set them. For local development, you'll need to set the callback URL to `http://localhost:3000/api/auth/callback/github`
+
+### Payments
+
+To test payments, you can set the following environment variables:
+
+```bash
+POLAR_ACCESS_TOKEN=test
+POLAR_WEBHOOK_SECRET=test
+```
+
+For testing payments, you'll need access to the polar sandbox which needs to be configured to point a webhook to your local server. This can be configured at:
+
+You'll need something like [ngrok](https://ngrok.com/) to expose your local server to the internet.
+
+```bash
+ngrok http http://localhost:3000
+```
+
+You'll need the webhook to point to ngrok like so:
+
+```
+https://0000-00-00-000-00.ngrok-free.app/api/auth/polar/webhooks
+```
+
+With this webhook pointing to your local server, you should be able to test payments.
+
+### Email sending
+
+Note, this is not required, if email sending is not configured, the app will log the email it would send to the console. Often this is more convenient for development.
+
+To test email sending, you can set the following environment variables:
+
+```bash
+SMTP_HOST=
+SMTP_USER=
+SMTP_PASS=
+SMTP_PORT=
+SMTP_SECURE=false
+```
+
+When configured, you'll be able to send emails to the email address you've configured.
+
+To setup with protonmail, you'll need to go to and create a new SMTP submission token.
+
+You'll need to set the following environment variables:
+
+```bash
+SMTP_HOST=smtp.protonmail.com
+SMTP_USER=my.email@protonmail.com
+SMTP_PASS=my-smtp-token
+SMTP_PORT=587
+SMTP_SECURE=false
+```
+
+# Contributing
+
+To submit your changes, open a pull request to the [BlockNote GitHub repo](https://github.com/TypeCellOS/BlockNote). Pull requests will automatically be deployed to a preview environment.
diff --git a/docs/app/(auth)/layout.tsx b/docs/app/(auth)/layout.tsx
new file mode 100644
index 0000000000..715e32a774
--- /dev/null
+++ b/docs/app/(auth)/layout.tsx
@@ -0,0 +1,10 @@
+import { HomeLayout } from "@/components/fumadocs/layout/home";
+import { baseOptions } from "@/lib/layout.shared";
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/docs/app/(auth)/signin/page.tsx b/docs/app/(auth)/signin/page.tsx
new file mode 100644
index 0000000000..b21d007847
--- /dev/null
+++ b/docs/app/(auth)/signin/page.tsx
@@ -0,0 +1,18 @@
+import AuthenticationPage from "@/components/AuthenticationPage";
+import { getFullMetadata } from "@/lib/getFullMetadata";
+import { Suspense } from "react";
+
+export const metadata = getFullMetadata({
+ title: "Sign In",
+ path: "/signin",
+});
+
+// Suspense + imported AuthenticationPage because AuthenticationPage is a client component
+// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/app/(auth)/signin/password/page.tsx b/docs/app/(auth)/signin/password/page.tsx
new file mode 100644
index 0000000000..d998c4db48
--- /dev/null
+++ b/docs/app/(auth)/signin/password/page.tsx
@@ -0,0 +1,17 @@
+import AuthenticationPage from "@/components/AuthenticationPage";
+import { Metadata } from "next";
+import { Suspense } from "react";
+
+export const metadata: Metadata = {
+ title: "Password Login",
+};
+
+// Suspense + imported AuthenticationPage because AuthenticationPage is a client component
+// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/app/(auth)/signup/page.tsx b/docs/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000000..04aceaa07a
--- /dev/null
+++ b/docs/app/(auth)/signup/page.tsx
@@ -0,0 +1,18 @@
+import AuthenticationPage from "@/components/AuthenticationPage";
+import { getFullMetadata } from "@/lib/getFullMetadata";
+import { Suspense } from "react";
+
+export const metadata = getFullMetadata({
+ title: "Sign Up",
+ path: "/signup",
+});
+
+// Suspense + imported AuthenticationPage because AuthenticationPage is a client component
+// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/app/(home)/_components/BlockCatalog.tsx b/docs/app/(home)/_components/BlockCatalog.tsx
new file mode 100644
index 0000000000..72804ec79e
--- /dev/null
+++ b/docs/app/(home)/_components/BlockCatalog.tsx
@@ -0,0 +1,108 @@
+"use client";
+import {
+ AudioWaveform,
+ ChevronRight,
+ Code2,
+ FileText,
+ Heading,
+ Image,
+ List,
+ ListOrdered,
+ ListTodo,
+ Minus,
+ Pilcrow,
+ Puzzle,
+ Quote,
+ Table,
+ Video,
+} from "lucide-react";
+import React from "react";
+
+const BlockCatalogItem: React.FC<{ name: string; icon: React.ReactNode }> = ({
+ name,
+ icon,
+}) => (
+
+ Every BlockNote document is a collection of blocks—headings, lists,
+ images, and more. Use the built-in blocks, customize them to fit
+ your needs, or create entirely new ones.
+
+ Three nations choose
+
+ open source
+ {" "}
+ to power
+
+ their digital future.
+
+
+ {/* Short punchy copy */}
+
+ France, Germany, and the Netherlands partner to build{" "}
+
+ Docs
+
+ , a collaborative writing tool for thousands of public servants.{" "}
+ BlockNote is the engine.
+
+
+ {/* Compelling social proof - simpler */}
+
+ "Building Digital Commons means better tools, data
+ sovereignty, and shared progress."
+
+
+ );
+};
diff --git a/docs/app/(home)/_components/FAQ.tsx b/docs/app/(home)/_components/FAQ.tsx
new file mode 100644
index 0000000000..158c5c691a
--- /dev/null
+++ b/docs/app/(home)/_components/FAQ.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+
+const faqs = [
+ {
+ question: "Isn't it easier to use a Headless editor framework?",
+ answer:
+ "There are a number of really powerful headless text editor frameworks available. In fact, BlockNote is built on Prosemirror and TipTap. However, even when using a headless library, it takes several months and requires deep expertise to build a fully-featured editor with a polished UI that your users expect.",
+ },
+ {
+ question: "Is BlockNote ready for production use?",
+ answer:
+ "BlockNote is used by dozens of companies in production, ranging from startups to large enterprises and public institutions. Also, we didn't reinvent the wheel. The core editor is built on top of Prosemirror - a battle tested framework that powers software from Atlassian, Gitlab, the New York Times, and many others.",
+ },
+ {
+ question: "Can I add my own extensions to BlockNote?",
+ answer:
+ "BlockNote comes with lot of functionality out-of-the-box, but we understand that every use case is different. You can easily customize the built-in UI Components, or create your own custom Blocks, Inline Content, and Styles. If you want to go even further, you can extend the core editor with additional Prosemirror or TipTap plugins.",
+ },
+ {
+ question: "Is BlockNote really free?",
+ answer:
+ "100% of BlockNote is open source. We offer consultancy, support services and commercial licenses for specific XL packages to help sustain BlockNote. Explore our pricing page for more details.",
+ },
+];
+
+export const FAQ: React.FC = () => {
+ return (
+
+
+ {tabs.map((tab) => {
+ const isActive = activeTabId === tab.id;
+ // Dynamic styles based on active state could be passed or handled here
+ // For simplicity, we'll use a generic active style or specific color logic if needed.
+ // But CodePlayground had specific colors (purple, amber, blue).
+ // Let's rely on the parent or use a generic active style here for now,
+ // or we can add a 'color' prop to FeatureTab if we want distinct colors per tab.
+
+ return (
+
+ );
+ })}
+
+ Build a Notion-style{" "}
+ editor in minutes.
+
+
+ The AI-native, open source rich
+ text editor for React. Add a{" "}
+ fully customizable modern block-based editing
+ experience to your product that users will love.
+
+
+
+
+ View Demo
+
+ →
+
+
+
+ Documentation
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/docs/app/(home)/_components/HeroVideo.tsx b/docs/app/(home)/_components/HeroVideo.tsx
new file mode 100644
index 0000000000..a36cccbe73
--- /dev/null
+++ b/docs/app/(home)/_components/HeroVideo.tsx
@@ -0,0 +1,77 @@
+"use client";
+import Link from "next/link";
+import React from "react";
+
+export const HeroVideo: React.FC = () => {
+ return (
+ <>
+
+
+ Building a rich text editor is one of the hardest engineering
+ challenges on the web. It used to take months of specialized
+ work.
+
+
+ We believe that great tools should be{" "}
+ sovereign by default. You shouldn't have
+ to choose between a cohesive UX and owning your
+ infrastructure.
+
+
+ That's why we built BlockNote. A{" "}
+ batteries-included editor that gives you a
+ Notion-quality experience in minutes, while staying grounded
+ in open standards like{" "}
+
+ ProseMirror
+ {" "}
+ and Yjs.
+
+
+
+
+ Whether you're a startup or a public institution, you
+ deserve software that lasts. Join us to{" "}
+
+ shape the future
+
+ {" "}
+ of the open web.
+
+
+
+
+
+ {/* Floating "Card" for Impact - DARK MODE */}
+
+
+
+
+ Enter BlockNote.
+
+
+ Forget low-level details. Work with a strongly typed API.
+ Get modern UI components out-of-the-box.
+
+);
diff --git a/docs/app/(home)/_components/OpenSource.tsx b/docs/app/(home)/_components/OpenSource.tsx
new file mode 100644
index 0000000000..d4c09b5ac0
--- /dev/null
+++ b/docs/app/(home)/_components/OpenSource.tsx
@@ -0,0 +1,126 @@
+"use client";
+import React from "react";
+
+const pillars = [
+ {
+ icon: "🏛️",
+ title: "Built on Giants",
+ description:
+ "ProseMirror and Yjs are battle-tested foundations trusted by teams worldwide, we're excited to build with these technologies.",
+ },
+ // {
+ // icon: "🤝",
+ // title: "Community First",
+ // description:
+ // "We collaborate closely with the Yjs team and contribute back to the ecosystem. Open source thrives on shared innovation.",
+ // },
+ // {
+ // icon: "🔓",
+ // title: "Yours to Own",
+ // description:
+ // "No vendor lock-in. Self-host, fork, extend. Your editing layer, under your control.",
+ // },
+ // {
+ // icon: "🇪🇺",
+ // title: "Digital Autonomy",
+ // description:
+ // "Partnering with DINUM (France) and Zendis (Germany) to build open European alternatives — reducing dependencies on big tech.",
+ // },
+ {
+ icon: "⬆️",
+ title: "Contributing Upstream",
+ description:
+ "We're significant contributors to Yjs, Hocuspocus, and Tiptap. When we improve the ecosystem, everyone benefits.",
+ },
+ {
+ icon: "🌱",
+ title: "Sustainable by Design",
+ description:
+ "Bootstrapped and independent. We're building for the long term, not the next funding round.",
+ },
+];
+
+export const OpenSource: React.FC = () => {
+ return (
+
+ {/* Subtle grid background */}
+
+
+
+
+
+
+
+ Committed to open source.
+
+
+ Document editing is foundational infrastructure for the modern
+ workforce. We believe the tools we use to create and share knowledge
+ should be open, transparent, and free from lock-in. That's why
+ everything we build is open source.
+
+
+
+
+ {pillars.map((pillar, index) => (
+
+
{pillar.icon}
+
{pillar.title}
+
+ {pillar.description}
+
+
+ ))}
+
+
+ {/* Founder Quote */}
+ {/*
+
+
+ "Here we could put a quote about our open source commitment."
+
+
+
+ >
+ );
+}
diff --git a/docs/app/pricing/faq.tsx b/docs/app/pricing/faq.tsx
new file mode 100644
index 0000000000..a7acc4b98f
--- /dev/null
+++ b/docs/app/pricing/faq.tsx
@@ -0,0 +1,140 @@
+import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
+import { Heading } from "fumadocs-ui/components/heading";
+import Link from "next/link";
+
+const faqs = [
+ {
+ question:
+ "What license is BlockNote using? Do I need a subscription to use BlockNote?",
+ answer: (
+ <>
+ We're proud to say that BlockNote is 100% open source software. The
+ core library is licensed under the{" "}
+ MPL 2.0 license,
+ which allows you to use BlockNote in commercial and closed-source
+ applications - even without a subscription. If you make changes to the
+ BlockNote source files, you're expected to publish these changes so
+ the wider community can benefit as well.
+
+ The XL packages (like AI integration, multi-column layouts, and
+ exporters) are dual-licensed and available under{" "}
+ GPL-3.0, or -
+ for closed-source projects - a commercial license as part of the
+ BlockNote Business subscription or above. See the{" "}
+
+ commercial license terms
+ {" "}
+ for the exact details.
+ >
+ ),
+ },
+ {
+ question: "When do I need a commercial license?",
+ answer: (
+ <>
+ Only when you use any of the XL packages (like AI integration,
+ multi-column layouts, and exporters) and you cannot comply with the
+ GPL-3.0 license you'll need a{" "}
+
+ commercial license
+
+ . This is likely to be the case when you're building closed-source
+ applications. The BlockNote Business subscription and above includes a
+ commercial license.
+ >
+ ),
+ },
+ {
+ question: "Why did you choose to dual-license the XL packages?",
+ answer: (
+ <>
+ We’ve built BlockNote as open source from day one and remain committed
+ to keeping the core library licensed under the MPL 2.0. That means it’s
+ free to use—even in commercial and closed-source projects.
+
+ To sustainably support ongoing development, we offer a small set of
+ advanced features (the XL packages) under a dual-license model:
+
+
GPL-3.0 for open-source projects
+
+ Commercial license (included in the BlockNote Business tier and
+ above) for closed-source use
+
+
+ This approach allows us to fund a full-time team while keeping 100% of
+ the code we build open source. It’s our way of balancing community
+ accessibility with long-term sustainability.
+ >
+ ),
+ },
+ {
+ question: "What kind of support is included in a license?",
+ answer: (
+ <>
+ We have you covered! All BlockNote subscriptions come with prioritized
+ support. See the{" "}
+
+ Service Level Agreement
+ {" "}
+ for the exact details.
+ >
+ ),
+ },
+ {
+ question:
+ "Is there any limit to the number of documents or users I can have?",
+ answer: `With BlockNote, there are no limits on the number of documents or users you can have.
+ You're free to run the software on your own infrastructure, and none of your data passes through our servers — your documents and users remain entirely your business.`,
+ },
+ {
+ question: "What if I have more than one SaaS or Web application?",
+ answer: (
+ <>
+ The BlockNote Commercial license (included in the Business tier and
+ above) for XL packages covers one application per license. See the{" "}
+
+ commercial license terms
+ {" "}
+ for the exact details.
+
+ If you want to use XL packages in more than one app, contact us at
+ team@blocknotejs.org; we're happy to work with you on a custom
+ license.
+ >
+ ),
+ },
+ {
+ question: "Do you offer any discounts for startups?",
+ answer: (
+ <>
+ Yes! We offer a discount for startups with less than 5 employees. See
+ the{" "}
+
+ commercial license terms
+ {" "}
+ for the exact details.
+ >
+ ),
+ },
+ {
+ question: "What payment methods do you accept?",
+ answer: `We accept all major credit cards. If you require a different payment method, please contact us.`,
+ },
+];
+
+export function FAQ() {
+ return (
+
+ );
+}
diff --git a/docs/app/pricing/layout.tsx b/docs/app/pricing/layout.tsx
new file mode 100644
index 0000000000..bbe4b9a500
--- /dev/null
+++ b/docs/app/pricing/layout.tsx
@@ -0,0 +1,6 @@
+import { HomeLayout } from "@/components/fumadocs/layout/home";
+import { baseOptions } from "@/lib/layout.shared";
+
+export default function Layout({ children }: LayoutProps<"/pricing">) {
+ return {children};
+}
diff --git a/docs/app/pricing/page.tsx b/docs/app/pricing/page.tsx
new file mode 100644
index 0000000000..ae4e110756
--- /dev/null
+++ b/docs/app/pricing/page.tsx
@@ -0,0 +1,213 @@
+import { FAQ } from "@/app/pricing/faq";
+import { Tier } from "@/app/pricing/tiers";
+import { InfiniteSlider } from "@/components/InfiniteSlider";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { getFullMetadata } from "@/lib/getFullMetadata";
+import Link from "next/link";
+import { PricingTiers } from "./PricingTiers";
+
+export const metadata = getFullMetadata({
+ title: "Pricing",
+ path: "/pricing",
+});
+
+const sponsors = [
+ // { name: "Semrush", logo: "/img/sponsors/semrush.light.png" },
+ // { name: "NLnet", logo: "/img/sponsors/nlnetLight.svg" },
+ { name: "DINUM", logo: "/img/sponsors/dinumLight.svg" },
+ { name: "ZenDiS", logo: "/img/sponsors/zendis.svg" },
+ { name: "OpenProject", logo: "/img/sponsors/openproject.svg" },
+ { name: "Poggio", logo: "/img/sponsors/poggioLight.svg" },
+ { name: "Capitol", logo: "/img/sponsors/capitolLight.svg" },
+ { name: "Twenty", logo: "/img/sponsors/twentyLight.png" },
+ { name: "Deep Origin", logo: "/img/sponsors/deepOrigin.svg" },
+ // { name: "Krisp", logo: "/img/sponsors/krisp.svg" },
+];
+
+const tiers: Tier[] = [
+ {
+ id: "free",
+ title: "Community",
+ icon: "💚",
+ tagline: "Get Started",
+ description: (
+ <>
+ Everything you need to get started.{" "}
+
+
+
+ Liberally licensed
+
+
+ BlockNote is MPL-licensed. This is close to MIT and free for any
+ use. The key difference is a "share-alike" requirement:
+ if you modify BlockNote's internal files, you must share
+ those specific changes.
+
+
+ {" "}
+ and free for any project.
+ >
+ ),
+ price: "Free",
+ features: [
+ "All blocks & UI components",
+
+ "Drag-and-drop editing",
+ "Slash commands & menus",
+ "Real-time collaboration",
+ "Comments",
+
+ XL Packages free for OSS under GPL-3.0
+ ,
+ ],
+ cta: "get-started",
+ href: "/docs",
+ },
+ {
+ id: "business",
+ title: "Business",
+ icon: "⚡",
+ tagline: "Go premium",
+ mostPopular: true,
+ badge: "Recommended",
+ description:
+ "Commercial license for access to advanced features and technical support.",
+ price: { month: 390, year: 2340 },
+ features: [
+
+ Commercial license for XL packages:
+ ,
+
+ • AI integration
+ ,
+
+ • Multi-column layouts
+ ,
+
+ • Export to PDF, Docx, ODT, Email
+ ,
+ "Logo on website and repositories",
+
+ Standard Support (
+
+ see SLA
+
+ )
+ ,
+ ],
+ cta: "buy",
+ },
+ {
+ id: "enterprise",
+ title: "Enterprise",
+ icon: "🏢",
+ tagline: "Sustainable partnerships",
+ description: "Custom licensing, dedicated support, and design partnership.",
+ price: "Custom",
+ features: [
+
+ Everything in Business, plus:
+ ,
+ "Custom BlockNote feature development",
+ "Private Slack channel with maintainers",
+ "Onboarding and integration guidance",
+
+ Priority Support (
+
+ see SLA
+
+ )
+ ,
+ ],
+ href: "mailto:team@blocknotejs.org",
+ cta: "contact",
+ },
+];
+
+export default function Pricing() {
+ return (
+
+
+ {/* Header */}
+
+
+ Pricing
+
+
+ 100% Open Source.
+
+
+ Fair Pricing.
+
+
+
+ The majority of BlockNote is liberally licensed and free to use for
+ any purpose. The dual-licensed XL features (like AI) are free for
+ open source projects, but require a commercial license for
+ closed-source applications.
+
+
+
+ {/* Pricing Tiers with Toggle */}
+
+
+ {/* Social proof */}
+
+
+ Trusted by teams building the future of collaboration
+
+
+ {sponsors.map((sponsor) => (
+
+
+
+ ))}
+
+
+
+ {/* Startup Discounts */}
+
+
+ Discounts for Startups
+
+
+ Building the next big thing? We love supporting early-stage
+ companies. If you're a seed-stage startup or non-profit, get in
+ touch for special pricing on our Business plan.
+
+ BlockNote is an extensible React rich text editor with support for
+ block-based editing, real-time collaboration, and comes with
+ ready-to-use customizable UI components.
+
+ );
+}
diff --git a/docs/components/fumadocs/ui/button.tsx b/docs/components/fumadocs/ui/button.tsx
new file mode 100644
index 0000000000..b427d4e080
--- /dev/null
+++ b/docs/components/fumadocs/ui/button.tsx
@@ -0,0 +1,28 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+
+const variants = {
+ primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
+ outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
+ ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
+ secondary:
+ 'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
+} as const;
+
+export const buttonVariants = cva(
+ 'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
+ {
+ variants: {
+ variant: variants,
+ // fumadocs use `color` instead of `variant`
+ color: variants,
+ size: {
+ sm: 'gap-1 px-2 py-1.5 text-xs',
+ icon: 'p-1.5 [&_svg]:size-5',
+ 'icon-sm': 'p-1.5 [&_svg]:size-4.5',
+ 'icon-xs': 'p-1 [&_svg]:size-4',
+ },
+ },
+ },
+);
+
+export type ButtonProps = VariantProps;
diff --git a/docs/components/fumadocs/ui/collapsible.tsx b/docs/components/fumadocs/ui/collapsible.tsx
new file mode 100644
index 0000000000..27079bd7b3
--- /dev/null
+++ b/docs/components/fumadocs/ui/collapsible.tsx
@@ -0,0 +1,32 @@
+'use client';
+import { Collapsible as Primitive } from '@base-ui/react/collapsible';
+import type { ComponentProps } from 'react';
+import { cn } from '../../../lib/fumadocs/cn';
+
+export const Collapsible = Primitive.Root;
+
+export const CollapsibleTrigger = Primitive.Trigger;
+
+export function CollapsibleContent({
+ children,
+ className,
+ ...props
+}: ComponentProps) {
+ return (
+
+ cn(
+ "overflow-hidden [&[hidden]:not([hidden='until-found'])]:hidden h-(--collapsible-panel-height) transition-[height] data-[starting-style]:h-0 data-[ending-style]:h-0",
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ >
+ {children}
+
+ );
+}
+
+export type CollapsibleProps = Primitive.Root.Props;
+export type CollapsibleContentProps = Primitive.Panel.Props;
+export type CollapsibleTriggerProps = Primitive.Trigger.Props;
diff --git a/docs/components/fumadocs/ui/navigation-menu.tsx b/docs/components/fumadocs/ui/navigation-menu.tsx
new file mode 100644
index 0000000000..af7f262651
--- /dev/null
+++ b/docs/components/fumadocs/ui/navigation-menu.tsx
@@ -0,0 +1,70 @@
+'use client';
+import * as React from 'react';
+import { NavigationMenu as Primitive } from '@base-ui/react/navigation-menu';
+import { cn } from '../../../lib/fumadocs/cn';
+
+export type NavigationMenuContentProps = Primitive.Content.Props;
+export type NavigationMenuTriggerProps = Primitive.Trigger.Props;
+
+const NavigationMenuRoot = Primitive.Root;
+
+const NavigationMenuList = Primitive.List;
+
+const NavigationMenuItem = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ cn('list-none', typeof className === 'function' ? className(s) : className)}
+ {...props}
+ >
+ {children}
+
+));
+
+NavigationMenuItem.displayName = Primitive.Item.displayName;
+
+const NavigationMenuTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ children, ...props }, ref) => (
+
+ {children}
+
+));
+NavigationMenuTrigger.displayName = Primitive.Trigger.displayName;
+
+const NavigationMenuContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+ cn(
+ 'size-full p-4',
+ 'transition-[opacity,transform,translate] duration-(--duration) ease-(--easing)',
+ 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',
+ 'data-[starting-style]:data-[activation-direction=left]:-translate-x-1/2',
+ 'data-[starting-style]:data-[activation-direction=right]:translate-x-1/2',
+ 'data-[ending-style]:data-[activation-direction=left]:translate-x-1/2',
+ 'data-[ending-style]:data-[activation-direction=right]:-translate-x-1/2',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ />
+));
+NavigationMenuContent.displayName = Primitive.Content.displayName;
+
+const NavigationMenuLink = Primitive.Link;
+
+export {
+ NavigationMenuRoot,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+};
diff --git a/docs/components/fumadocs/ui/popover.tsx b/docs/components/fumadocs/ui/popover.tsx
new file mode 100644
index 0000000000..8955bdcdbd
--- /dev/null
+++ b/docs/components/fumadocs/ui/popover.tsx
@@ -0,0 +1,34 @@
+'use client';
+import { Popover as Primitive } from '@base-ui/react/popover';
+import * as React from 'react';
+import { cn } from '../../../lib/fumadocs/cn';
+
+const Popover = Primitive.Root;
+
+const PopoverTrigger = Primitive.Trigger;
+
+const PopoverContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef &
+ Pick
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
+
+
+
+ cn(
+ 'z-50 origin-(--transform-origin) overflow-y-auto max-h-(--available-height) min-w-[240px] max-w-[98vw] rounded-xl border bg-fd-popover/60 backdrop-blur-lg p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[closed]:animate-fd-popover-out data-[open]:animate-fd-popover-in',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ />
+
+
+));
+PopoverContent.displayName = Primitive.Popup.displayName;
+
+const PopoverClose = Primitive.Close;
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
diff --git a/docs/components/fumadocs/ui/scroll-area.tsx b/docs/components/fumadocs/ui/scroll-area.tsx
new file mode 100644
index 0000000000..fbdbce0baf
--- /dev/null
+++ b/docs/components/fumadocs/ui/scroll-area.tsx
@@ -0,0 +1,65 @@
+import { ScrollArea as Primitive } from '@base-ui/react/scroll-area';
+import * as React from 'react';
+import { cn } from '../../../lib/fumadocs/cn';
+
+const ScrollArea = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ cn('overflow-hidden', typeof className === 'function' ? className(s) : className)
+ }
+ {...props}
+ >
+ {children}
+
+
+
+));
+
+ScrollArea.displayName = Primitive.Root.displayName;
+
+const ScrollViewport = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ cn('size-full rounded-[inherit]', typeof className === 'function' ? className(s) : className)
+ }
+ {...props}
+ >
+ {children}
+
+));
+
+ScrollViewport.displayName = Primitive.Viewport.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+ cn(
+ 'flex select-none transition-opacity',
+ !s.hovering && 'opacity-0',
+ orientation === 'vertical' && 'h-full w-1.5',
+ orientation === 'horizontal' && 'h-1.5 flex-col',
+ typeof className === 'function' ? className(s) : className,
+ )
+ }
+ {...props}
+ >
+
+
+));
+ScrollBar.displayName = Primitive.Scrollbar.displayName;
+
+export { ScrollArea, ScrollBar, ScrollViewport };
+export type ScrollAreaProps = Primitive.Root.Props;
diff --git a/docs/components/gradients.module.css b/docs/components/gradients.module.css
new file mode 100644
index 0000000000..1e0ce34cf7
--- /dev/null
+++ b/docs/components/gradients.module.css
@@ -0,0 +1,137 @@
+.barBorder {
+ border: rgba(255, 255, 255, 0.4) 1px solid;
+ :global(.light) & {
+ border: rgba(0, 0, 0, 0.6) 1px solid;
+ }
+}
+
+.tooltipArrow {
+ display: block;
+ border-left: 8px solid transparent;
+ border-bottom: 8px solid #333333;
+ border-right: 8px solid transparent;
+ :global(.light) & {
+ border-bottom: 8px solid #f5f5f5;
+ }
+}
+.translatingGlow {
+ background: linear-gradient(32deg, #2a8af6 0%, #a853ba 50%, #e92a67 100%);
+ background-size: 200% 200%;
+ animation: translateGlow 7s linear infinite;
+ will-change: filter;
+}
+
+@keyframes translateGlow {
+ 0% {
+ background-position: -20% -20%;
+ }
+ 25% {
+ background-position: 30% 80%;
+ }
+ 50% {
+ background-position: 110% 110%;
+ }
+ 75% {
+ background-position: 80% 30%;
+ }
+ 100% {
+ background-position: -20% -20%;
+ }
+}
+
+.heroHeading {
+ background: linear-gradient(180deg, #ffffff 0%, #aaaaaa 100%), #ffffff;
+ :global(.light) & {
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, #000000 100%);
+ background-clip: text;
+ }
+ background-clip: text;
+}
+
+.letterLine {
+ opacity: 0.2;
+ background: linear-gradient(
+ 90deg,
+ #000000 0%,
+ #ffffff 20%,
+ #ffffff 80%,
+ #000000 100%
+ );
+ :global(.light) & {
+ background: linear-gradient(
+ 90deg,
+ #ffffff 0%,
+ #000000 20%,
+ #000000 80%,
+ #ffffff 100%
+ );
+ }
+}
+
+.glow {
+ mix-blend-mode: normal;
+ filter: blur(75px);
+ will-change: filter;
+}
+
+.glowSmall {
+ filter: blur(32px);
+}
+
+.glowBlue {
+ background: linear-gradient(180deg, #58a5ff 0%, #a67af4 100%);
+}
+
+.glowPink {
+ background: linear-gradient(180deg, #ff3358 0%, #ff4fd8 100%);
+}
+
+.glowConic {
+ background: conic-gradient(
+ from 180deg at 50% 50%,
+ #2a8af6 0deg,
+ #a853ba 180deg,
+ #e92a67 360deg
+ );
+}
+
+.glowGray {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.gradientSectionBorder {
+ --gradient-y-offset: -200px;
+ --gradient-x-offset: -200px;
+ --height: 255px;
+ position: relative;
+ overflow: hidden;
+ will-change: filter;
+}
+
+.gradientSectionBorderLeft {
+ position: absolute;
+ width: 60vw;
+ height: var(--height);
+ left: var(--gradient-x-offset);
+ top: var(--gradient-y-offset);
+ background: linear-gradient(180deg, #58a5ff 0%, #a67af4 100%);
+ border-radius: 100%;
+ mix-blend-mode: normal;
+ filter: blur(50px);
+}
+
+.gradientSectionBorderRight {
+ width: 60vw;
+ position: absolute;
+ height: var(--height);
+ right: var(--gradient-x-offset);
+ top: var(--gradient-y-offset);
+ background: linear-gradient(180deg, #ff3358 0%, #ff4fd8 100%);
+ border-radius: 100%;
+ mix-blend-mode: normal;
+ filter: blur(50px);
+}
+
+.gradientSectionBorderDivider {
+ background: linear-gradient(90deg, #288cf9 0%, #e32c6b 100%);
+}
diff --git a/docs/components/provider.tsx b/docs/components/provider.tsx
new file mode 100644
index 0000000000..848c6bc845
--- /dev/null
+++ b/docs/components/provider.tsx
@@ -0,0 +1,17 @@
+"use client";
+import SearchDialog from "@/components/search";
+import { RootProvider } from "fumadocs-ui/provider/next";
+import { type ReactNode } from "react";
+
+export function Provider({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/docs/components/search.tsx b/docs/components/search.tsx
new file mode 100644
index 0000000000..1f704205fd
--- /dev/null
+++ b/docs/components/search.tsx
@@ -0,0 +1,46 @@
+'use client';
+import {
+ SearchDialog,
+ SearchDialogClose,
+ SearchDialogContent,
+ SearchDialogHeader,
+ SearchDialogIcon,
+ SearchDialogInput,
+ SearchDialogList,
+ SearchDialogOverlay,
+ type SharedProps,
+} from 'fumadocs-ui/components/dialog/search';
+import { useDocsSearch } from 'fumadocs-core/search/client';
+import { create } from '@orama/orama';
+import { useI18n } from 'fumadocs-ui/contexts/i18n';
+
+function initOrama() {
+ return create({
+ schema: { _: 'string' },
+ // https://docs.orama.com/docs/orama-js/supported-languages
+ language: 'english',
+ });
+}
+
+export default function DefaultSearchDialog(props: SharedProps) {
+ const { locale } = useI18n(); // (optional) for i18n
+ const { search, setSearch, query } = useDocsSearch({
+ type: 'static',
+ initOrama,
+ locale,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/components/ui/tooltip.tsx b/docs/components/ui/tooltip.tsx
new file mode 100644
index 0000000000..53c5f20146
--- /dev/null
+++ b/docs/components/ui/tooltip.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { cn } from "@/lib/fumadocs/cn";
+import { Tooltip as Primitive } from "@base-ui/react/tooltip";
+import * as React from "react";
+
+const TooltipProvider = ({ children }: { children: React.ReactNode }) => (
+ <>{children}>
+);
+
+const Tooltip = Primitive.Root;
+
+const TooltipTrigger = Primitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef &
+ Pick
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+ cn(
+ "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border border-stone-200 bg-white px-3 py-1.5 text-xs text-stone-700 shadow-md",
+ typeof className === "function" ? className(s) : className,
+ )
+ }
+ {...props}
+ />
+
+
+));
+TooltipContent.displayName = Primitive.Popup.displayName;
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/docs/content/docs/features/ai/backend-integration.mdx b/docs/content/docs/features/ai/backend-integration.mdx
new file mode 100644
index 0000000000..51b7adf216
--- /dev/null
+++ b/docs/content/docs/features/ai/backend-integration.mdx
@@ -0,0 +1,113 @@
+---
+title: Backend Integration
+description: Integrate BlockNote AI with your backend
+imageTitle: BlockNote AI Backend Integration
+---
+
+# Backend Integration with BlockNote AI
+
+The most common (and recommended) setup to integrate BlockNote AI with an LLM is to have BlockNote AI call your backend, which then calls an LLM of your choice using the [Vercel AI SDK](https://ai-sdk.dev/docs/foundations/overview). This page explains the default setup, but also provides several alternative approaches.
+
+## Default setup (Vercel AI SDK)
+
+The example below closely follows the [basic example from the Vercel AI SDK](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#example) for Next.js.
+The only difference is that we're retrieving the BlockNote tools from the request body and using the `toolDefinitionsToToolSet` function to convert them to AI SDK tools. We also forward the serialized document state (selection, cursor, block IDs) that BlockNote adds to every user message by calling `injectDocumentStateMessages`. The LLM will now be able to invoke these tools to make modifications to the BlockNote document as requested by the user. The tool calls are forwarded to the client application where they're handled automatically by the AI Extension.
+
+```ts app/api/chat/route.ts
+import { openai } from "@ai-sdk/openai";
+import { convertToModelMessages, streamText } from "ai";
+import {
+ aiDocumentFormats,
+ injectDocumentStateMessages,
+ toolDefinitionsToToolSet,
+} from "@blocknote/xl-ai/server";
+
+// Allow streaming responses up to 30 seconds
+export const maxDuration = 30;
+
+export async function POST(req: Request) {
+ const { messages, toolDefinitions } = await req.json();
+
+ const result = streamText({
+ model: openai("gpt-4.1"), // see https://ai-sdk.dev/docs/foundations/providers-and-models
+ system: aiDocumentFormats.html.systemPrompt,
+ messages: await convertToModelMessages(
+ injectDocumentStateMessages(messages),
+ ),
+ tools: toolDefinitionsToToolSet(toolDefinitions),
+ toolChoice: "required",
+ });
+
+ return result.toUIMessageStreamResponse();
+}
+```
+
+Different javascript frameworks will have a very similar setup. For example, see our [Hono example](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-ai-server/src/routes/regular.ts).
+
+If your backend is in another language, you're unable to use Vercel AI SDK, or you can't setup a backend at all - there are several alternatives to integrate BlockNote AI:
+
+## Data Stream Protocol
+
+BlockNote AI expects your backend to respond with Server-Sent Events (SSE) data streams according to the Data Stream Protocol. This protocol specified by the [Vercel AI SDK](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol#data-stream-protocol). You can use this information to develop custom backends for your use case. For example, to provide compatible API endpoints that are implemented in a different language such as Python.
+
+- Here's an [example using Python FastAPI](https://github.com/vercel/ai/blob/main/examples/next-fastapi/api/index.py) as a backend (note [this](https://github.com/vercel/ai/issues/7496#issuecomment-3181826620) open issue).
+
+## Custom transport
+
+Instead of modifying your backend to support the Data Stream Protocol, you can also implement a custom [transport layer](https://ai-sdk.dev/docs/ai-sdk-ui/transport) in the client application. The transport layer determines how AI SDK requests are sent to your backend to retrieve an LLM response.
+
+## ClientSideTransport
+
+BlockNote AI also provides a `ClientSideTransport` class that can be used to connect directly to LLMs without routing through a backend. To use this transport, create a Vercel AI SDK Provider and LanguageModel directly on the client. Then, use this to instantiate the transport and pass it to the AI Extension.
+
+The example below uses the [OpenAI Compatible](https://ai-sdk.dev/providers/openai-compatible-providers) provider, but you can use any [provider / model](https://ai-sdk.dev/docs/foundations/providers-and-models) you want.
+
+```ts
+import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
+import { ClientSideTransport } from "@blocknote/xl-ai";
+
+const model = createOpenAICompatible({
+ apiKey: 'your-api-key',
+ baseURL: 'https://your-provider',
+})('model-id');
+
+// ...
+ AIExtension({
+ transport: new ClientSideTransport({
+ model,
+ }),
+}),
+// ...
+```
+
+### With a proxy server
+
+It's likely you cannot call your LLM provider directly in your client application using `ClientSideTransport`, because you need to hide API keys or prevent [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) issues. For this reason, you can use a proxy server to route requests to your LLM provider. This proxy server can then inject your API keys and forward the request to your LLM provider.
+
+BlockNote AI provides a `fetchViaProxy` function that can be used to create a fetch function that routes requests through a proxy server (the example below uses Groq as the LLM provider):
+
+```ts
+import { createGroq } from "@ai-sdk/groq";
+import { fetchViaProxy } from "@blocknote/xl-ai";
+
+const model = createGroq({
+ fetch: fetchViaProxy(
+ (url) => `${BASE_URL}/proxy?provider=groq&url=${encodeURIComponent(url)}`,
+ ),
+ apiKey: "fake-api-key", // the API key is not used as it's actually added in the proxy server
+})("llama-3.3-70b-versatile");
+```
+
+- See [a full example](https://github.com/TypeCellOS/BlockNote/blob/main/examples/09-ai/06-client-side-transport/src/App.tsx) of how to use `ClientSideTransport` with a proxy server.
+
+# Advanced patterns
+
+You can connect BlockNote AI features with more advanced AI pipelines. You can integrate concepts like Agents, RAG (Retrieval-Augmented Generation), multi-step LLM calls. Make sure you always expose the BlockNote tools (as passed via the `toolDefinitions` in the request body) to the LLM and forward invocations to the client.
+
+
+ We love to hear about your integrations and collaborate on advanced AI
+ patterns. For dedicated support on integrating your pipeline and application
+ with BlockNote AI, [get in touch](/about).
+
+
+- By default, BlockNote AI sends the entire LLM chat history to the backend. See [the server persistence example](https://github.com/TypeCellOS/BlockNote/tree/main/examples/09-ai/07-server-persistence) for a pattern where the backend stores chat and only the latest message is sent to the backend.
diff --git a/docs/content/docs/features/ai/custom-commands.mdx b/docs/content/docs/features/ai/custom-commands.mdx
new file mode 100644
index 0000000000..4bf760dcd8
--- /dev/null
+++ b/docs/content/docs/features/ai/custom-commands.mdx
@@ -0,0 +1,124 @@
+---
+title: Custom AI Commands
+description: Customize the AI menu items (commands) in your BlockNote rich text editor
+---
+
+# Custom AI Menu Items (commands)
+
+A central part when users are interacting with the AI agent is the _AI Suggestion Menu_ where users can enter a custom prompt or select a pre-defined command:
+
+
+
+This menu is easy to customize so you can expose commands fine-tuned to your application.
+
+## Defining your own commands
+
+First, we define a new AI command. Let's create one that makes selected text more informal.
+
+```tsx
+import {
+ AIExtension,
+ AIMenuSuggestionItem,
+ aiDocumentFormats,
+} from "@blocknote/xl-ai";
+
+// Custom item to make the text more informal.
+export const makeInformal = (
+ editor: BlockNoteEditor,
+): AIMenuSuggestionItem => ({
+ key: "make_informal",
+ title: "Make Informal",
+ aliases: ["informal", "make informal", "casual"],
+ icon: ,
+ onItemClick: async () => {
+ await editor.getExtension(AIExtension)?.invokeAI({
+ // The prompt to send to the LLM:
+ userPrompt: "Give the selected text a more informal (casual) tone",
+ // Tell the LLM to specifically use the selected content as context (instead of the whole document)
+ useSelection: true,
+ // We only want the LLM to update selected text, not to add / delete blocks
+ streamToolsProvider: aiDocumentFormats.html.getStreamToolsProvider({
+ defaultStreamTools: {
+ add: false,
+ delete: false,
+ update: true,
+ },
+ }),
+ });
+ },
+ size: "small",
+});
+```
+
+Now, we create a customized AI Menu to show this command when the user has selected some text and opened the AI menu:
+
+```tsx
+import { AIMenu, getDefaultAIMenuItems } from "@blocknote/xl-ai";
+
+function CustomAIMenu() {
+ return (
+ ,
+ aiResponseStatus:
+ | "user-input"
+ | "thinking"
+ | "ai-writing"
+ | "error"
+ | "user-reviewing"
+ | "closed",
+ ) => {
+ if (aiResponseStatus === "user-input") {
+ if (editor.getSelection()) {
+ // When a selection is active (so when the AI Menu is opened via the Formatting Toolbar),
+ // we add our `makeInformal` command to the default items.
+ return [
+ ...getDefaultAIMenuItems(editor, aiResponseStatus),
+ makeInformal(editor),
+ ];
+ } else {
+ return getDefaultAIMenuItems(editor, aiResponseStatus);
+ }
+ }
+
+ // for other states, return the default items
+ return getDefaultAIMenuItems(editor, aiResponseStatus);
+ }}
+ />
+ );
+}
+```
+
+Now, let's use this custom AI Menu in our app:
+
+```tsx
+
+ {/* Creates a new AIMenu with the default items, as well as our custom
+ ones. */}
+
+
+ {/* ...other UI Elements... */}
+
+
+
+```
+
+# Full example
+
+Have a look at the full example below, where we also add an AI menu item when no selection is open
+(e.g., when the editor is opened by typing `/ai` in the editor).
+
+
+
+# Reference
+
+So far, we've added basic commands to the editor, but it's possible to completely customize low level prompts sent to the LLM.
+To learn in detail about the `invokeAI` method used in this guide, continue to the [AI reference docs](/docs/features/ai/reference).
diff --git a/docs/content/docs/features/ai/getting-started.mdx b/docs/content/docs/features/ai/getting-started.mdx
new file mode 100644
index 0000000000..602d9d7d6a
--- /dev/null
+++ b/docs/content/docs/features/ai/getting-started.mdx
@@ -0,0 +1,126 @@
+---
+title: Getting Started
+description: Add AI functionality to your BlockNote rich text editor
+imageTitle: Getting Started with BlockNote AI
+---
+
+# Getting Started with BlockNote AI
+
+This guide walks you through the steps to add AI functionality to your BlockNote rich text editor.
+
+First, install the `@blocknote/xl-ai` package:
+
+```bash
+npm install @blocknote/xl-ai
+```
+
+BlockNote AI uses the the [AI SDK](https://ai-sdk.dev/docs/foundations/overview) to standardize integrating artificial intelligence (AI) models across [supported providers](https://ai-sdk.dev/docs/foundations/providers-and-models).
+
+## Setting up the editor
+
+Let's create an editor with the AI Extension enabled:
+
+```ts
+import { createBlockNoteEditor } from "@blocknote/core";
+import { BlockNoteAIExtension } from "@blocknote/xl-ai";
+import { en } from "@blocknote/core/locales";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import { AIExtension } from "@blocknote/xl-ai";
+import "@blocknote/xl-ai/style.css"; // add the AI stylesheet
+
+const editor = createBlockNoteEditor({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ extensions: [
+ AIExtension({
+ transport: new DefaultChatTransport({
+ api: `/api/chat`,
+ }),
+ }),
+ ],
+ // ... other editor options
+});
+```
+
+See the [API Reference](/docs/features/ai/reference) for more information on the `AIExtension` options.
+
+## Adding AI UI elements
+
+The next step is to customize the UI elements of your editor.
+We want to:
+
+- add an AI button to the formatting toolbar (shown when selecting text)
+- add an AI option to the slash menu (shown when typing a `/`)
+
+We do this by disabling the default FormattingToolbar and SuggestionMenu and adding our own. See [Changing UI Elements](/docs/react/components) for more information.
+
+```tsx
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* Create you own Formatting Toolbar with an AI button,
+ (see the full example code below) */}
+
+
+ {/* Create you own SlashMenu with an AI option,
+ (see the full example code below) */}
+
+
+```
+
+## Backend setup
+
+Now, we'll set up a backend route to handle AI requests that will be forwarded to your LLM provider.
+
+This example follows the [basic example from the AI SDK](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#example) for Next.js:
+
+```ts app/api/chat/route.ts
+import { openai } from "@ai-sdk/openai";
+import { convertToModelMessages, streamText } from "ai";
+import {
+ aiDocumentFormats,
+ injectDocumentStateMessages,
+ toolDefinitionsToToolSet,
+} from "@blocknote/xl-ai/server";
+
+// Allow streaming responses up to 30 seconds
+export const maxDuration = 30;
+
+export async function POST(req: Request) {
+ const { messages, toolDefinitions } = await req.json();
+
+ const result = streamText({
+ model: openai("gpt-4.1"), // see https://ai-sdk.dev/docs/foundations/providers-and-models
+ system: aiDocumentFormats.html.systemPrompt,
+ messages: await convertToModelMessages(
+ injectDocumentStateMessages(messages),
+ ),
+ tools: toolDefinitionsToToolSet(toolDefinitions),
+ toolChoice: "required",
+ });
+
+ return result.toUIMessageStreamResponse();
+}
+```
+
+This follows the regular `streamText` pattern of the AI SDK, with 3 exceptions:
+
+- the BlockNote document state is extracted from message metadata and injected into the messages, using `injectDocumentStateMessages`
+- BlockNote client-side tool definitions are extracted from the request body and passed to the LLM using `toolDefinitionsToToolSet`
+- The system prompt is set to the default BlockNote system prompt (`aiDocumentFormats.html.systemPrompt`). You can override or extend the system prompt. If you do so, make sure your modified system prompt still explains the AI on how to modify the BlockNote document.
+
+See [Backend integrations](/docs/features/ai/backend-integration) for more information on how to integrate BlockNote AI with your backend.
+
+# Full Example
+
+See the full example code and live demo. Select some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.
+
+
diff --git a/docs/content/docs/features/ai/index.mdx b/docs/content/docs/features/ai/index.mdx
new file mode 100644
index 0000000000..a5ad9c6ca3
--- /dev/null
+++ b/docs/content/docs/features/ai/index.mdx
@@ -0,0 +1,67 @@
+---
+title: AI Rich Text Editing
+description: Add AI functionality to your BlockNote rich text editor
+imageTitle: BlockNote AI Integration
+---
+
+# BlockNote AI Integration
+
+With BlockNote AI, you can add AI functionality to your rich text editor.
+Users can work with an AI agent to edit, write and format their documents.
+
+
+
+
+ BlockNote AI is now in early preview - we'd love to hear your feedback! We
+ also collaborate with companies to help with the integration or implement more
+ advanced AI related functionality. [Get in touch](/about).
+
+
+## Features
+
+BlockNote AI has been designed to be fully customizable.
+BlockNote shows exactly what the AI agent is doing - making it easy for users to
+work hand in hand with AI agents.
+
+### User Experience
+
+- **Interactive AI Suggestions**: Users can accept or reject AI suggestions with a simple click, maintaining full control over their content
+- **Real-time Feedback**: Streaming support provides immediate responses, making the AI interaction feel natural and responsive
+- **Transparent Operations**: See exactly what the AI is doing at each step, with clear visual indicators of AI actions
+
+### Technical Capabilities
+
+- **Flexible Command System**: Customize commands to write or update existing content and formatting
+- **Multi-step Workflows**: Support for "Human in the Loop" workflows where users can guide the AI
+- **Model Agnostic**: Connect any LLM model (from Llama to OpenAI, Mistral or Anthropic)
+- **Customizable Prompts**: Fine-tune AI behavior with custom prompts and instructions
+- **No Backend Required**: Use your own infrastructure or connect directly to a hosted LLM API
+
+### Integration
+
+- **Built on Vercel AI SDK**: Leverages the power of the [Vercel AI SDK](https://www.ai-sdk.dev) for reliable AI integration
+- **RAG**: Easily extend the knowledge of BlockNote AI with your own RAG (Retrieval-Augmented Generation) pipeline
+- **Easy Setup**: Get started quickly with minimal configuration
+- **Completely Customizable**: Fully customizable commands and UI elements
+
+
+ This feature is provided by the `@blocknote/xl-ai` package. `xl-` packages are
+ fully open source, but released under a copyleft license. A commercial license
+ for usage in closed source, proprietary products comes as part of the
+ [Business subscription](/pricing).
+
+
+### Next Steps
+
+- Try the [basic AI demo](/examples/ai/minimal) to see it in action
+- Explore different models in the [AI playground](/examples/ai/playground)
+- Check out the [setup documentation](/docs/features/ai/getting-started)
+- Learn about [customizing commands](/docs/features/ai/custom-commands)
+- Review the [API Reference](/docs/features/ai/reference)
+
+Have a feature request or need help with integration? [Get in touch](/about) with our team.
diff --git a/docs/content/docs/features/ai/meta.json b/docs/content/docs/features/ai/meta.json
new file mode 100644
index 0000000000..18e7406d17
--- /dev/null
+++ b/docs/content/docs/features/ai/meta.json
@@ -0,0 +1,9 @@
+{
+ "title": "AI",
+ "pages": [
+ "getting-started",
+ "backend-integration",
+ "custom-commands",
+ "reference"
+ ]
+}
diff --git a/docs/content/docs/features/ai/reference.mdx b/docs/content/docs/features/ai/reference.mdx
new file mode 100644
index 0000000000..ee8f305b5c
--- /dev/null
+++ b/docs/content/docs/features/ai/reference.mdx
@@ -0,0 +1,243 @@
+---
+title: BlockNote AI Reference
+description: Reference documentation for the BlockNote AI extension
+---
+
+# AI Reference
+
+## `AIExtension`
+
+Use `AIExtension` to create a new AI Extension that can be registered to an editor when calling `useCreateBlockNote`.
+
+```typescript
+// Usage:
+useCreateBlockNote({
+ // Register the AI extension
+ extensions: [AIExtension(options)],
+ // other editor options
+});
+
+type AIExtensionOptions = AIRequestHelpers & {
+ /**
+ * The name and color of the agent cursor when the AI is writing
+ * @default { name: "AI", color: "#8bc6ff" }
+ */
+ agentCursor?: { name: string; color: string };
+};
+
+type AIRequestHelpers = {
+ /**
+ * Transport used by the AI SDK to send requests to your backend/LLM.
+ * Implement to customize backend URLs or use a different transport (e.g. websockets).
+ */
+ transport?: ChatTransport;
+
+ /**
+ * Use the ChatProvider to customize how the AI SDK Chat instance is created.
+ * For example, when you want to reuse an existing Chat instance used in the rest of your application.
+ *
+ * @note you cannot use both `chatProvider` and `transport` together.
+ */
+ chatProvider?: () => Chat;
+
+ /**
+ * Customize which stream tools are available to the LLM.
+ */
+ streamToolsProvider?: StreamToolsProvider;
+ // Provide `streamToolsProvider` in AIExtension(options) or override per call via InvokeAIOptions.
+ // If omitted, defaults to using `aiDocumentFormats.html.getStreamToolsProvider()`.
+
+ /**
+ * Extra options (headers/body/metadata) forwarded to the AI SDK request.
+ */
+ chatRequestOptions?: ChatRequestOptions;
+
+ /**
+ * Build the serializable document state that will be forwarded to the backend.
+ *
+ * @default aiDocumentFormats.html.defaultDocumentStateBuilder
+ */
+ documentStateBuilder?: DocumentStateBuilder;
+};
+```
+
+## `AIExtension` extension instance
+
+The `AIExtension` extension instance returned by `editor.getExtension(AIExtension)` exposes state and methods to interact with BlockNote's AI features.
+
+```typescript
+type AIExtensionInstance = {
+ /**
+ * Execute a call to an LLM and apply the result to the editor
+ */
+ invokeAI(opts: InvokeAIOptions): Promise;
+
+ /**
+ * Returns a read-only Tanstack Store with the state of the AI Menu
+ */
+ get store(): Store<{
+ aiMenuState:
+ | ({
+ /**
+ * The ID of the block that the AI menu is opened at.
+ * This changes as the AI is making changes to the document
+ */
+ blockId: string;
+ } & (
+ | {
+ status: "error";
+ error: any;
+ }
+ | {
+ status:
+ | "user-input"
+ | "thinking"
+ | "ai-writing"
+ | "user-reviewing";
+ }
+ ))
+ | "closed";
+ }>;
+
+ /**
+ * Returns a Tanstack Store with the global configuration of the AI Extension.
+ * These options are used by default across all LLM calls when calling {@link invokeAI}
+ */
+ readonly options: Store;
+
+ /** Open the AI menu at a specific block */
+ openAIMenuAtBlock(blockID: string): void;
+ /** Close the AI menu */
+ closeAIMenu(): void;
+ /** Accept the changes made by the LLM */
+ acceptChanges(): void;
+ /** Reject the changes made by the LLM */
+ rejectChanges(): void;
+ /** Retry the previous LLM call (only valid when status is "error") */
+ retry(): Promise;
+ /** Abort the current LLM request */
+ abort(reason?: any): Promise;
+ /** Advanced: manually update the status shown by the AI menu */
+ setAIResponseStatus(
+ status:
+ | "user-input"
+ | "thinking"
+ | "ai-writing"
+ | "user-reviewing"
+ | { status: "error"; error: any },
+ ): void;
+};
+```
+
+### `InvokeAI`
+
+Requests to an LLM are made by calling `invokeAI` on the `AIExtension` instance. This takes an `InvokeAIOptions` object as an argument.
+
+```typescript
+type InvokeAIOptions = {
+ /** The user prompt */
+ userPrompt: string;
+
+ /** Whether to use the editor selection for the LLM call (default: true) */
+ useSelection?: boolean;
+
+ /**
+ * If the user's cursor is in an empty paragraph, automatically delete it when the AI starts writing.
+ * Used when typing `/ai` in an empty block. (default: true)
+ */
+ deleteEmptyCursorBlock?: boolean;
+} & AIRequestHelpers; // Optionally override helpers per request
+```
+
+Because `InvokeAIOptions` extends `AIRequestHelpers`, you can override these options on a per-call basis without changing the global extension configuration.
+
+## `getStreamToolsProvider`
+
+When an LLM is called, it needs to interpret the document and invoke operations to modify it. Use a format's `getStreamToolsProvider` to obtain the tools the LLM may call while editing. In most cases, use `aiDocumentFormats.html.getStreamToolsProvider(...)`.
+
+```typescript
+/** Return a provider for the stream tools available to the LLM */
+type getStreamToolsProvider = (
+ // Whether to add artificial delays between document edits
+ // or apply them immediately as they're streamed in from the LLM without delays
+ // (default: true)
+ withDelays: boolean,
+ // The stream tools to use, there are separate tools for adding, updating and deleting blocks
+ // (default: { add: true, update: true, delete: true })
+ defaultStreamTools?: {
+ add?: boolean;
+ update?: boolean;
+ delete?: boolean;
+ },
+) => StreamToolsProvider;
+```
+
+## Document state builders (advanced)
+
+When BlockNote AI sends a request it also forwards a serialized snapshot of the editor. LLMs use this document state to understand document, cursor position and active selection. The `DocumentStateBuilder` type defines how that snapshot is produced:
+
+```typescript
+type DocumentStateBuilder = (
+ aiRequest: Omit,
+) => Promise<
+ | {
+ selection: false;
+ blocks: BlocksWithCursor[];
+ isEmptyDocument: boolean;
+ }
+ | {
+ selection: true;
+ selectedBlocks: { id: string; block: T }[];
+ blocks: { block: T }[];
+ isEmptyDocument: boolean;
+ }
+>;
+```
+
+By default, `aiDocumentFormats.html.defaultDocumentStateBuilder` is used.
+
+## `AIRequest` (advanced)
+
+`buildAIRequest` returns everything BlockNote AI needs to execute an AI call:
+
+```typescript
+type AIRequest = {
+ editor: BlockNoteEditor;
+ selectedBlocks?: Block[];
+ emptyCursorBlockToDelete?: string;
+ streamTools: StreamTool[];
+ documentState: DocumentState;
+ onStart: () => void;
+};
+```
+
+## `sendMessageWithAIRequest` (advanced)
+
+Use `sendMessageWithAIRequest` when you need to manually call the LLM without updating the state of the BlockNote AI menu.
+For example, you could use this when you want to submit LLM requests from a different context (e.g.: a chat window).
+`sendMessageWithAIRequest` is similar to `chat.sendMessages`, but it attaches the `documentState` to the outgoing message metadata, configures tool streaming, and forwards tool definitions (JSON Schemas) to your backend.
+
+```typescript
+async function sendMessageWithAIRequest(
+ chat: Chat,
+ aiRequest: AIRequest,
+ message?: Parameters["sendMessage"]>[0],
+ options?: Parameters["sendMessage"]>[1],
+): Promise>;
+```
+
+## `buildAIRequest` (advanced)
+
+Use `buildAIRequest` to assemble an `AIRequest` from editor state if you are bypassing `invokeAI` and call `sendMessageWithAIRequest` directly.
+
+```typescript
+async function buildAIRequest(opts: {
+ editor: BlockNoteEditor;
+ useSelection?: boolean;
+ deleteEmptyCursorBlock?: boolean;
+ streamToolsProvider?: StreamToolsProvider;
+ documentStateBuilder?: DocumentStateBuilder;
+ onBlockUpdated?: (blockId: string) => void;
+ onStart?: () => void;
+}): Promise;
+```
diff --git a/docs/content/docs/features/blocks/code-blocks.mdx b/docs/content/docs/features/blocks/code-blocks.mdx
new file mode 100644
index 0000000000..8f5d1816b3
--- /dev/null
+++ b/docs/content/docs/features/blocks/code-blocks.mdx
@@ -0,0 +1,122 @@
+---
+title: Code Blocks
+description: How to add syntax highlighting to code blocks.
+---
+
+# Code Blocks
+
+Code blocks are a simple way to display formatted code with syntax highlighting.
+
+Code blocks by default are a simple way to display code. But, BlockNote also supports more advanced features like:
+
+- Syntax highlighting
+- Custom themes
+- Multiple languages
+- Tab indentation
+
+
+ These features are disabled by default to keep the default code block
+ experience easy to use and reduce bundle size. They can be individually added
+ when [configuring the
+ block](/docs/features/blocks#configuring-default-blocks).
+
+
+**Configuration Options**
+
+```ts
+type CodeBlockOptions = {
+ indentLineWithTab?: boolean;
+ defaultLanguage?: string;
+ supportedLanguages?: Record<
+ string,
+ {
+ name: string;
+ aliases?: string[];
+ }
+ >;
+ createHighlighter?: () => Promise>;
+};
+```
+
+`indentLineWithTab:` Whether the Tab key should indent lines, or not be handled by the code block specially. Defaults to `true`.
+
+`defaultLanguage:` The syntax highlighting default language for code blocks which are created/inserted without a set language, which is `text` by default (no syntax highlighting).
+
+`supportedLanguages:` The syntax highlighting languages supported by the code block, which is an empty array by default.
+
+`createHighlighter:` The [Shiki highliter](https://shiki.style/guide/load-theme) to use for syntax highlighting.
+
+BlockNote also provides a generic set of options for syntax highlighting in the `@blocknote/code-block` package, which support a wide range of languages:
+
+```ts
+import { createCodeBlockSpec } from "@blocknote/core";
+import { codeBlockOptions } from "@blocknote/code-block";
+
+const codeBlock = createCodeBlockSpec(codeBlockOptions);
+```
+
+See [this example](/examples/theming/code-block) to see it in action.
+
+**Type & Props**
+
+```ts
+type CodeBlock = {
+ id: string;
+ type: "codeBlock";
+ props: {
+ language: string;
+ };
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+`language:` The syntax highlighting language to use. Defaults to `text`, which has no highlighting.
+
+## Custom Syntax Highlighting
+
+To create your own syntax highlighter, you can use the [shiki-codegen](https://shiki.style/packages/codegen) CLI for generating the code to create one for your chosen languages and themes.
+
+For example, to create a syntax highlighter using the optimized javascript engine, javascript, typescript, vue, with light and dark themes, you can run the following command:
+
+```bash
+npx shiki-codegen --langs javascript,typescript,vue --themes light-plus,dark-plus --engine javascript --precompiled ./shiki.bundle.ts
+```
+
+This will generate a `shiki.bundle.ts` file that you can use to create a syntax highlighter for your editor.
+
+Like this:
+
+```ts
+import { createHighlighter } from "./shiki.bundle.js";
+
+export default function App() {
+ const editor = useCreateBlockNote({
+ schema: BlockNoteSchema.create().extend({
+ blockSpecs: {
+ codeBlock: createCodeBlockSpec({
+ indentLineWithTab: true,
+ defaultLanguage: "typescript",
+ supportedLanguages: {
+ typescript: {
+ name: "TypeScript",
+ aliases: ["ts"],
+ },
+ },
+ createHighlighter: () =>
+ createHighlighter({
+ themes: ["light-plus", "dark-plus"],
+ langs: [],
+ }),
+ }),
+ },
+ }),
+ });
+
+ return ;
+}
+```
+
+See the custom code block example for a more detailed example.
+
+
diff --git a/docs/content/docs/features/blocks/custom.mdx b/docs/content/docs/features/blocks/custom.mdx
new file mode 100644
index 0000000000..5735a5dd3e
--- /dev/null
+++ b/docs/content/docs/features/blocks/custom.mdx
@@ -0,0 +1,9 @@
+---
+title: Custom
+description: How to create custom blocks, inline content and styles in BlockNote.
+---
+
+# Custom Blocks, Inline Content and Styles
+
+You can also extend your editor and create your own Blocks, Inline Content or Styles using React.
+Skip to [Custom Schemas (advanced)](/docs/features/custom-schemas) to learn how to do this.
diff --git a/docs/content/docs/features/blocks/embeds.mdx b/docs/content/docs/features/blocks/embeds.mdx
new file mode 100644
index 0000000000..726d32f116
--- /dev/null
+++ b/docs/content/docs/features/blocks/embeds.mdx
@@ -0,0 +1,167 @@
+---
+title: Embeds
+description: How to use embeds in BlockNote.
+---
+
+# Embed Blocks
+
+Embeds are a way to display content from external sources in your documents. BlockNote supports various embeds to help you structure and format your content effectively.
+
+## File
+
+**Type & Props**
+
+```ts
+type FileBlock = {
+ id: string;
+ type: "file";
+ props: {
+ backgroundColor: string;
+ name: string;
+ url: string;
+ caption: string;
+ };
+ content: undefined;
+ children: Block[];
+};
+```
+
+`backgroundColor:` The background color of the block. Defaults to `"default"`.
+
+`name:` The file name. Defaults to `""`.
+
+`url:` The file URL. Defaults to `""`.
+
+`caption:` The file caption. Defaults to `""`.
+
+## Image
+
+**Configuration Options**
+
+```ts
+type ImageBlockOptions = {
+ icon?: string;
+};
+```
+
+`icon:` The HTML string for an icon SVG. If no URL is given, the image block displays a button to add one using a link or file, and the provided SVG replaces the generic file icon in that button.
+
+**Type & Props**
+
+```typescript
+type ImageBlock = {
+ id: string;
+ type: "image";
+ props: {
+ backgroundColor: string;
+ textAlignment: "left" | "center" | "right" | "justify";
+ name: string;
+ url: string;
+ caption: string;
+ showPreview: boolean;
+ previewWidth: number | undefined;
+ };
+ content: undefined;
+ children: Block[];
+};
+```
+
+`backgroundColor:` The background color of the block. Defaults to `"default"`.
+
+`textAlignment:` The alignment of the image. Defaults to `"left"`.
+
+`name:` The image file name. Defaults to `""`.
+
+`url:` The image URL. Defaults to `""`.
+
+`caption:` The image caption. Defaults to `""`.
+
+`showPreview:` Whether to show the image preview or a link. Defaults to `true`.
+
+`previewWidth:` The image preview width in pixels. Defaults to `undefined` (no fixed width).
+
+## Video
+
+**Configuration Options**
+
+```ts
+type VideoBlockOptions = {
+ icon?: string;
+};
+```
+
+`icon:` The HTML string for an icon SVG. If no URL is given, the video block displays a button to add one using a link or file, and the provided SVG replaces the generic file icon in that button.
+
+**Type & Props**
+
+```ts
+type VideoBlock = {
+ id: string;
+ type: "video";
+ props: {
+ backgroundColor: string;
+ textAlignment: "left" | "center" | "right" | "justify";
+ name: string;
+ url: string;
+ caption: string;
+ showPreview: boolean;
+ previewWidth: number | undefined;
+ };
+ content: undefined;
+ children: Block[];
+};
+```
+
+`backgroundColor:` The background color of the block. Defaults to `"default"`.
+
+`textAlignment:` The alignment of the video. Defaults to `"left"`.
+
+`name:` The video file name. Defaults to `""`.
+
+`url:` The video URL. Defaults to `""`.
+
+`caption:` The video caption. Defaults to `""`.
+
+`showPreview:` Whether to show the video player or a link. Defaults to `true`.
+
+`previewWidth:` The video preview width in pixels. Defaults to `undefined` (no fixed width).
+
+## Audio
+
+**Configuration Options**
+
+```ts
+type AudioBlockOptions = {
+ icon?: string;
+};
+```
+
+`icon:` The HTML string for an icon SVG. If no URL is given, the audio block displays a button to add one using a link or file, and the provided SVG replaces the generic file icon in that button.
+
+**Type & Props**
+
+```ts
+type AudioBlock = {
+ id: string;
+ type: "audio";
+ props: {
+ backgroundColor: string;
+ name: string;
+ url: string;
+ caption: string;
+ showPreview: boolean;
+ };
+ content: undefined;
+ children: Block[];
+};
+```
+
+`backgroundColor:` The background color of the block. Defaults to `"default"`.
+
+`name:` The audio file name. Defaults to `""`.
+
+`url:` The audio URL. Defaults to `""`.
+
+`caption:` The audio caption. Defaults to `""`.
+
+`showPreview:` Whether to show the audio player or a link. Defaults to `true`.
diff --git a/docs/content/docs/features/blocks/index.mdx b/docs/content/docs/features/blocks/index.mdx
new file mode 100644
index 0000000000..7e07b6f773
--- /dev/null
+++ b/docs/content/docs/features/blocks/index.mdx
@@ -0,0 +1,72 @@
+---
+title: Built-in Blocks
+description: BlockNote supports a variety of built-in block and inline content types that are included in the editor by default.
+---
+
+# Built-in Blocks
+
+BlockNote supports a number of built-in blocks, inline content types, and styles that are included in the editor by default. This is called the Default Schema. To create your own content types, see [Custom Schemas](/docs/features/custom-schemas).
+
+The demo below showcases each of BlockNote's built-in block and inline content types:
+
+
+
+## Default Block Properties
+
+There are some default block props that BlockNote uses for the built-in blocks:
+
+```typescript twoslash
+type DefaultProps = {
+ /**
+ * The background color of the block, which also applies to nested blocks.
+ * @default "default"
+ */
+ backgroundColor: string;
+ /**
+ * The text color of the block, which also applies to nested blocks.
+ * @default "default"
+ */
+ textColor: string;
+ /**
+ * The text alignment of the block.
+ * @default "left"
+ */
+ textAlignment: "left" | "center" | "right" | "justify";
+};
+```
+
+## Configuring Default Blocks
+
+Some default blocks can be configured with options. For example, headings can be configured to have different available levels:
+
+```ts
+// Creates a new instance of the default heading block.
+const heading = createHeadingBlockSpec({
+ // Sets the block to support only heading levels 1-3.
+ levels: [1, 2, 3],
+});
+```
+
+Each default block type can be instantiated using their respective `create...BlockSpec` function. If the block can be configured, i.e. if it has options, you can pass them in an object to the function. To see which options each block type supports, read on to the next pages.
+
+To add your configured block to the editor, you must pass in a [custom schema](/docs/features/custom-schemas) with it. The simplest way to do this is by [extending the default schema](/docs/features/custom-schemas#extending-an-existing-schema):
+
+```ts
+const editor = useCreateBlockNote({
+ // Creates a default schema and extends it with the configured heading block.
+ schema: BlockNoteSchema.create().extend({
+ blockSpecs: {
+ heading: createHeadingBlockSpec({
+ // Sets the allowed heading levels.
+ levels: [1, 2, 3],
+ }),
+ },
+ }),
+});
+```
+
+You can see this in action in a working demo [here](/examples/custom-schema/configuring-blocks).
+
+## Explore
+
+
diff --git a/docs/content/docs/features/blocks/inline-content.mdx b/docs/content/docs/features/blocks/inline-content.mdx
new file mode 100644
index 0000000000..a22e93f19c
--- /dev/null
+++ b/docs/content/docs/features/blocks/inline-content.mdx
@@ -0,0 +1,173 @@
+---
+title: Inline Content
+description: How to use inline content in BlockNote.
+---
+
+# Inline Content
+
+By default, `InlineContent` (the content of text blocks like paragraphs) in BlockNote can either be a `StyledText` or a `Link` object.
+
+Here's an overview of all default inline content and the properties they support:
+
+## Styled Text
+
+`StyledText` is a type of `InlineContent` used to display pieces of text with styles:
+
+```typescript twoslash
+/**
+ * Styles can be applied to text.
+ */
+type Styles = {
+ bold: boolean;
+ italic: boolean;
+ underline: boolean;
+ strike: boolean;
+ code: boolean;
+ textColor: string;
+ backgroundColor: string;
+};
+
+// ---cut---
+type StyledText = {
+ type: "text";
+ /**
+ * The text content.
+ */
+ text: string;
+ /**
+ * The styles of the text.
+ */
+ styles: Styles;
+};
+```
+
+## Link
+
+`Link` objects represent links to a URL:
+
+```typescript twoslash
+type Styles = {
+ bold: boolean;
+ italic: boolean;
+ underline: boolean;
+ strike: boolean;
+ code: boolean;
+ textColor: string;
+ backgroundColor: string;
+};
+
+/**
+ * Any text content within a link
+ */
+type StyledText = {
+ type: "text";
+ text: string;
+ styles: Styles;
+};
+
+// ---cut---
+type Link = {
+ type: "link";
+ /**
+ * The content of the link.
+ */
+ content: StyledText[];
+ /**
+ * The href of the link.
+ */
+ href: string;
+};
+```
+
+### Customizing Links
+
+You can customize how links are rendered and how they respond to clicks with the `links` editor option.
+
+```ts
+const editor = BlockNoteEditor.create({
+ links: {
+ HTMLAttributes: {
+ class: "my-link-class",
+ target: "_blank",
+ },
+ onClick: (event) => {
+ // Custom click logic, e.g. routing without a page reload.
+ },
+ },
+});
+```
+
+#### `HTMLAttributes`
+
+Additional HTML attributes that should be added to rendered link elements.
+
+```ts
+const editor = BlockNoteEditor.create({
+ links: {
+ HTMLAttributes: {
+ class: "my-link-class",
+ target: "_blank",
+ },
+ },
+});
+```
+
+#### `onClick`
+
+Custom handler invoked when a link is clicked. If left `undefined`, links are opened in a new window on click (the default behavior). If provided, that default behavior is disabled and this function is called instead.
+
+Returning `false` will let BlockNote run other click handlers after this one. Returning `true` or nothing (the default) marks the event as handled.
+
+```ts
+const editor = BlockNoteEditor.create({
+ links: {
+ onClick: (event) => {
+ // Do something when a link is clicked.
+ },
+ },
+});
+```
+
+## Default Styles
+
+The default text formatting options in BlockNote are represented by the `Styles` in the default schema:
+
+```typescript twoslash
+type Styles = {
+ /**
+ * Whether the text is bold.
+ * @default false
+ */
+ bold: boolean;
+ /**
+ * Whether the text is italic.
+ * @default false
+ */
+ italic: boolean;
+ /**
+ * Whether the text is underlined.
+ * @default false
+ */
+ underline: boolean;
+ /**
+ * Whether the text is struck through.
+ * @default false
+ */
+ strike: boolean;
+ /**
+ * Whether the text is rendered as inline code.
+ * @default false
+ */
+ code: boolean;
+ /**
+ * The text color.
+ * @default "default"
+ */
+ textColor: string;
+ /**
+ * The background color of the text.
+ * @default "default"
+ */
+ backgroundColor: string;
+};
+```
diff --git a/docs/content/docs/features/blocks/list-types.mdx b/docs/content/docs/features/blocks/list-types.mdx
new file mode 100644
index 0000000000..edc941c1c4
--- /dev/null
+++ b/docs/content/docs/features/blocks/list-types.mdx
@@ -0,0 +1,80 @@
+---
+title: List Types
+description: How to use list types in BlockNote.
+---
+
+# List Item Blocks
+
+List item blocks are used to create different types of lists in your documents. BlockNote supports various list item blocks to help you structure and format your content effectively.
+
+### Bullet List Item
+
+A bullet list item is a list item that is not numbered.
+
+**Type & Props**
+
+```typescript
+type BulletListItemBlock = {
+ id: string;
+ type: "bulletListItem";
+ props: DefaultProps;
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+### Numbered List Item
+
+A numbered list item is a list item that is numbered.
+
+**Type & Props**
+
+```typescript
+type NumberedListItemBlock = {
+ id: string;
+ type: "numberedListItem";
+ props: DefaultProps & {
+ start?: number;
+ };
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+`start:` The number of this list item. If not provided, it defaults to `1`, or is incremented from the previous item.
+
+### Check List Item
+
+A check list item is a list item that can be checked or unchecked.
+
+**Type & Props**
+
+```typescript
+type CheckListItemBlock = {
+ id: string;
+ type: "checkListItem";
+ props: DefaultProps & {
+ checked: boolean;
+ };
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+`checked:` Whether the list item is checked or not.
+
+### Toggle List Item
+
+A toggle list item is a list item that can show or hide it's children.
+
+**Type & Props**
+
+```typescript
+type ToggleListItemBlock = {
+ id: string;
+ type: "toggleListItem";
+ props: DefaultProps;
+ content: InlineContent[];
+ children: Block[];
+};
+```
diff --git a/docs/content/docs/features/blocks/meta.json b/docs/content/docs/features/blocks/meta.json
new file mode 100644
index 0000000000..cf603446fc
--- /dev/null
+++ b/docs/content/docs/features/blocks/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Built-in Blocks",
+ "pages": [
+ "typography",
+ "list-types",
+ "tables",
+ "embeds",
+ "code-blocks",
+ "inline-content",
+ "custom",
+ "..."
+ ]
+}
diff --git a/docs/content/docs/features/blocks/tables.mdx b/docs/content/docs/features/blocks/tables.mdx
new file mode 100644
index 0000000000..e0d3a84e0b
--- /dev/null
+++ b/docs/content/docs/features/blocks/tables.mdx
@@ -0,0 +1,129 @@
+---
+title: Tables
+description: How to use tables in BlockNote.
+---
+
+# Table Blocks
+
+Tables are a simple way to display data in a grid.
+
+Tables by default are a simple way to display data in a grid. But, BlockNote also supports more advanced features like:
+
+- Split cells
+- Cell background color
+- Cell text color
+- Header rows & columns
+
+
+ These features are disabled by default to keep the default table experience
+ easy to use.
+
+
+You can enable more advanced features by passing the `tables` option when creating the editor.
+
+```ts
+const editor = new BlockNoteEditor({
+ tables: {
+ splitCells: true,
+ cellBackgroundColor: true,
+ cellTextColor: true,
+ headers: true,
+ },
+});
+```
+
+You can choose to enable only certain features, or none at all. Giving you the flexibility to use tables how you need in your app.
+
+## Block Shape
+
+This describes the shape of a table block in BlockNote.
+
+```ts
+type TableBlock = {
+ id: string;
+ type: "table";
+ props: {
+ textColor: string;
+ };
+ content: TableContent;
+ children: Block[];
+};
+
+type TableContent = {
+ type: "tableContent";
+ columnWidths: (number | undefined)[];
+ headerRows?: number;
+ headerCols?: number;
+ rows: {
+ cells: TableCell[];
+ }[];
+};
+
+type TableCell = {
+ type: "tableCell";
+ props: {
+ backgroundColor: string;
+ textColor: string;
+ textAlignment: "left" | "center" | "right" | "justify";
+ colspan?: number;
+ rowspan?: number;
+ };
+ content: InlineContent[];
+};
+```
+
+`textColor:` The text color of the table block. Defaults to `"default"`.
+
+## Options
+
+### Cell background color
+
+To enable cell background color, you need to pass `cellBackgroundColor: true` to the `tables` option.
+
+```ts
+const editor = new BlockNoteEditor({
+ tables: {
+ cellBackgroundColor: true,
+ },
+});
+```
+
+### Cell text color
+
+To enable cell text color, you need to pass `cellTextColor: true` to the `tables` option.
+
+```ts
+const editor = new BlockNoteEditor({
+ tables: {
+ cellTextColor: true,
+ },
+});
+```
+
+### Header rows & columns
+
+BlockNote supports headers in tables, which are the first row and/or first column of a table.
+
+To enable it, you need to pass `headers: true` to the `tables` option.
+
+```ts
+const editor = new BlockNoteEditor({
+ tables: {
+ headers: true,
+ },
+});
+```
+
+### Split cells
+
+Splitting and merging cells is a common feature of more advanced table editors.
+
+To enable it, you need to pass `splitCells: true` to the `tables` option.
+
+```ts
+const editor = new BlockNoteEditor({
+ tables: {
+ splitCells: true,
+ },
+});
+```
diff --git a/docs/content/docs/features/blocks/typography.mdx b/docs/content/docs/features/blocks/typography.mdx
new file mode 100644
index 0000000000..a0d5940fe4
--- /dev/null
+++ b/docs/content/docs/features/blocks/typography.mdx
@@ -0,0 +1,98 @@
+---
+title: Typography
+description: How to use typography blocks in BlockNote.
+---
+
+# Typography Blocks
+
+Typography blocks are fundamental elements for displaying text content in your documents. BlockNote supports various typography blocks to help you structure and format your content effectively.
+
+## Paragraph
+
+**Type & Props**
+
+```typescript
+type ParagraphBlock = {
+ id: string;
+ type: "paragraph";
+ props: DefaultProps;
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+## Heading
+
+**Configuration Options**
+
+```typescript
+type HeadingBlockOptions = Partial<{
+ defaultLevel?: number;
+ levels?: number[];
+ allowToggleHeadings?: boolean;
+}>;
+```
+
+`defaultLevel:` The default level for headings which are created/inserted without a set level, which is `1` by default.
+
+`levels:` The heading levels that the block supports, which is `[1, 2, 3, 4, 5, 6]` by default.
+
+`allowToggleHeadings:` Whether toggle headings should be supported, `true` by default. Toggle headings have a button which toggles between hiding and showing the block's children.
+
+**Type & Props**
+
+```typescript
+type HeadingBlock = {
+ id: string;
+ type: "heading";
+ props: {
+ level: 1 | 2 | 3 | 4 | 5 | 6 = 1;
+ isToggleable?: boolean;
+ } & DefaultProps;
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+`level:` The heading level, representing a title (`level: 1`), heading (`level: 2`), and subheadings (`level: 3`, `level: 4`, `level: 5`, `level: 6`). Defaults to `1`.
+
+`isToggleable:` Whether the heading is toggled (children hidden). Only present when `allowToggleHeadings` is `true` (the default). Defaults to `false`.
+
+## Quote
+
+**Type & Props**
+
+```typescript
+type QuoteBlock = {
+ id: string;
+ type: "quote";
+ props: {
+ backgroundColor: string;
+ textColor: string;
+ };
+ content: InlineContent[];
+ children: Block[];
+};
+```
+
+`backgroundColor:` The background color of the block. Defaults to `"default"`.
+
+`textColor:` The text color of the block. Defaults to `"default"`.
+
+## Divider
+
+A horizontal rule used to separate content.
+
+**Type & Props**
+
+```typescript
+type DividerBlock = {
+ id: string;
+ type: "divider";
+ props: {};
+ content: undefined;
+ children: Block[];
+};
+```
+
+The divider block has no props and no content. It can be inserted by typing `---` on an empty line.
diff --git a/docs/content/docs/features/collaboration/comments.mdx b/docs/content/docs/features/collaboration/comments.mdx
new file mode 100644
index 0000000000..4b88225993
--- /dev/null
+++ b/docs/content/docs/features/collaboration/comments.mdx
@@ -0,0 +1,167 @@
+---
+title: Comments
+description: Learn how to enable comments in your BlockNote editor
+---
+
+# Comments
+
+BlockNote supports Comments, Comment Threads (replies) and emoji reactions out of the box.
+
+To enable comments in your editor, you need to:
+
+- Create an instance of the `CommentsExtension` and pass it to the `extensions` editor option.
+- Pass `resolveUsers` to your `CommentsExtension` instance, so it can retrieve and display user information (names and avatars).
+- Provide a `ThreadStore` to your `CommentsExtension` instance, so it can store and retrieve comment threads.
+- Enable real-time collaboration (see [Real-time collaboration](/docs/features/collaboration))
+- Optionally provide a schema for comments and comment editors to use. If left undefined, they will use the [default comment editor schema](https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/Comments/defaultCommentEditorSchema.ts). See [here](/docs/features/custom-schemas) to find out more about custom schemas.
+
+```tsx
+const editor = useCreateBlockNote({
+ extensions: [
+ CommentsExtension({
+ // See below.
+ threadStore: ...,
+ // Return user information for the given userIds (see below).
+ resolveUsers: async (userIds: string[]) => { ... },
+ // Optional, can be left undefined
+ schema: BlockNoteSchema.create(...)
+ }),
+ ...
+ ],
+ collaboration: {
+ // See real-time collaboration docs
+ ...
+ },
+ ...
+});
+```
+
+**Demo**
+
+
+
+## ThreadStores
+
+A ThreadStore is used to store and retrieve comment threads. BlockNote is backend agnostic, so you can use any database or backend to store the threads.
+BlockNote comes with several built-in ThreadStore implementations:
+
+### `YjsThreadStore`
+
+The `YjsThreadStore` provides direct Yjs-based storage for comments, storing thread data directly in the Yjs document. This implementation is ideal for simple collaborative setups where all users have write access to the document.
+
+```tsx
+import { YjsThreadStore } from "@blocknote/core/comments";
+
+const threadStore = new YjsThreadStore(
+ userId, // The active user's ID
+ yDoc.getMap("threads"), // Y.Map to store threads
+ new DefaultThreadStoreAuth(userId, "editor"), // Authorization information, see below
+);
+```
+
+_Note: While this is the easiest to implement, it requires users to have write access to the Yjs document to leave comments. Also, without proper server-side validation, any user could technically modify other users' comments._
+
+### `RESTYjsThreadStore`
+
+The `RESTYjsThreadStore` combines Yjs storage with a REST API backend, providing secure comment management while maintaining real-time collaboration. This implementation is ideal when you have strong authentication requirements, but is a little more work to set up.
+
+In this implementation, data is written to the Yjs document via a REST API which can handle access control. Data is still retrieved from the Yjs document directly (after it's been updated by the REST API), this way all comment information automatically syncs between clients using the existing collaboration provider.
+
+```tsx
+import {
+ RESTYjsThreadStore,
+ DefaultThreadStoreAuth,
+} from "@blocknote/core/comments";
+
+const threadStore = new RESTYjsThreadStore(
+ "https://api.example.com/comments", // Base URL for the REST API
+ {
+ Authorization: "Bearer your-token", // Optional headers to add to requests
+ },
+ yDoc.getMap("threads"), // Y.Map to retrieve commend data from
+ new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
+);
+```
+
+An example implementation of the REST API can be found in the [example repository](https://github.com/TypeCellOS/BlockNote-demo-nextjs-hocuspocus).
+
+_Note: Because writes are executed via a REST API, the `RESTYjsThreadStore` is not suitable for local-first applications that should be able to add and edit comments offline._
+
+### `TiptapThreadStore`
+
+The `TiptapThreadStore` integrates with Tiptap's collaboration provider for comment management. This implementation is designed specifically for use with Tiptap's collaborative editing features.
+
+```tsx
+import {
+ TiptapThreadStore,
+ DefaultThreadStoreAuth,
+} from "@blocknote/core/comments";
+import { TiptapCollabProvider } from "@tiptap-pro/provider";
+
+// Create a TiptapCollabProvider (you probably have this already)
+const provider = new TiptapCollabProvider({
+ name: "test",
+ baseUrl: "https://collab.yourdomain.com",
+ appId: "test",
+ document: doc,
+});
+
+// Create a TiptapThreadStore
+const threadStore = new TiptapThreadStore(
+ userId, // The active user's ID
+ provider, // Tiptap collaboration provider
+ new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
+);
+```
+
+### ThreadStoreAuth
+
+The `ThreadStoreAuth` class defines the authorization rules for interacting with comments. Every ThreadStore implementation requires a `ThreadStoreAuth` instance. BlockNote uses the `ThreadStoreAuth` instance to deterine which interactions are allowed for the current user (for example, whether they can create a new comment, edit or delete a comment, etc.).
+
+The `DefaultThreadStoreAuth` class provides a basic implementation of the `ThreadStoreAuth` class. It takes a user ID and a role ("comment" or "editor") and implements the rules. See the [source code](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/comments/threadstore/DefaultThreadStoreAuth.ts) for more details.
+
+_Note: The `ThreadStoreAuth` only used to show / hide options in the UI. To secure comment related data, you still need to implement your own server-side validation (e.g. using `RESTYjsThreadStore` and a secure REST API)._
+
+## `resolveUsers` function
+
+When a user interacts with a comment, the data is stored in the ThreadStore, along with the active user ID (as specified when initiating the ThreadStore).
+
+To display comments, BlockNote needs to retrieve user information (such as the username and avatar) based on the user ID. To do this, you need to provide a `resolveUsers` function to your `CommentsExtension`.
+
+This function is called with an array of user IDs, and should return an array of `User` objects in the same order.
+
+```tsx
+type User = {
+ id: string;
+ username: string;
+ avatarUrl: string;
+};
+
+async function myResolveUsers(userIds: string[]): Promise {
+ // fetch user information from your database / backend
+ // and return an array of User objects
+
+ return await callYourBackend(userIds);
+
+ // Return a list of users
+ return users;
+}
+```
+
+## Sidebar View
+
+BlockNote also offers a different way of viewing and interacting with comments, via a sidebar instead of floating in the editor, using the `ThreadsSidebar` component:
+
+
+
+The only requirement for `ThreadsSidebar` is that it should be placed somewhere within your `BlockNoteView`, other than that you can position and style it however you want.
+
+`ThreadsSidebar` also takes 2 props:
+
+**`filter`**: Filter the comments in the sidebar. Can pass `"open"`, `"resolved"`, or `"all"`, to only show open, resolved, or all comments. Defaults to `"all"`.
+
+**`sort`**: Sort the comments in the sidebar. Can pass `"position"`, `"recent-activity"`, or `"oldest"`. Sorting by `"recent-activity"` uses the most recently added comment to sort threads, while `"oldest"` uses the thread creation date. Sorting by `"position"` puts comments in the same order as their reference text in the editor. Defaults to `"position"`.
+
+**`maxCommentsBeforeCollapse`**: The maximum number of comments that can be in a thread before the replies get collapsed. Defaults to 5.
+
+See [here](https://playground.blocknotejs.org/collaboration/comments-with-sidebar?hideMenu=true) for a standalone example of the `ThreadsSidebar` component.
diff --git a/docs/content/docs/features/collaboration/index.mdx b/docs/content/docs/features/collaboration/index.mdx
new file mode 100644
index 0000000000..2d320ab829
--- /dev/null
+++ b/docs/content/docs/features/collaboration/index.mdx
@@ -0,0 +1,104 @@
+---
+title: Real-time Collaboration
+description: Learn how to create multiplayer experiences with BlockNote
+---
+
+# Real-time Collaboration (Multiplayer Text Editor)
+
+Let's see how you can add Multiplayer capabilities to your BlockNote setup, and allow real-time collaboration between users (similar to Google Docs):
+
+
+
+_Try the live demo on the [homepage](https://www.blocknotejs.org)_
+
+BlockNote uses [Yjs](https://github.com/yjs/yjs) for this, and you can set it up with the `collaboration` option:
+
+```typescript
+import * as Y from "yjs";
+import { WebrtcProvider } from "y-webrtc";
+// ...
+
+const doc = new Y.Doc();
+
+const provider = new WebrtcProvider("my-document-id", doc); // setup a yjs provider (explained below)
+const editor = useCreateBlockNote({
+ // ...
+ collaboration: {
+ // The Yjs Provider responsible for transporting updates:
+ provider,
+ // Where to store BlockNote data in the Y.Doc:
+ fragment: doc.getXmlFragment("document-store"),
+ // Information (name and color) for this user:
+ user: {
+ name: "My Username",
+ color: "#ff0000",
+ },
+ // When to show user labels on the collaboration cursor. Set by default to
+ // "activity" (show when the cursor moves), but can also be set to "always".
+ showCursorLabels: "activity",
+ },
+ // ...
+});
+```
+
+## Yjs Providers
+
+When a user edits the document, an incremental change (or "update") is captured and can be shared between users of your app. You can share these updates by setting up a _Yjs Provider_. In the snipped above, we use [y-webrtc](https://github.com/yjs/y-webrtc) which shares updates over WebRTC (and BroadcastChannel), but you might be interested in different providers for production-ready use cases.
+
+- [Liveblocks](https://liveblocks.io/yjs) A fully hosted WebSocket infrastructure and persisted data store for Yjs documents. Includes webhooks, REST API, and browser DevTools, all for Yjs
+- [PartyKit](https://www.partykit.io/) A serverless provider that runs on Cloudflare
+- [Y-Sweet](https://jamsocket.com/y-sweet) An open-source provider that runs fully managed on [Jamsocket](https://jamsocket.com) or self-hosted in your own cloud
+- [Hocuspocus](https://www.hocuspocus.dev/) open source and extensible Node.js server with pluggable storage (scales with Redis)
+- [y-websocket](https://github.com/yjs/y-websocket) provider that you can connect to your own websocket server
+- [y-indexeddb](https://github.com/yjs/y-indexeddb) for offline storage
+- [y-webrtc](https://github.com/yjs/y-webrtc) transmits updates over WebRTC
+- [Matrix-CRDT](https://github.com/yousefED/matrix-crdt) syncs updates over Matrix (experimental)
+- [Nostr-CRDT](https://github.com/yousefED/nostr-crdt) syncs updates over Nostr (experimental)
+
+## Liveblocks
+
+Liveblocks provides a hosted back-end for Yjs. You can create a fully-featured example project which uses Liveblocks with BlockNote by running the command below (you will need a Liveblocks account for this):
+
+```shell
+npx create-liveblocks-app@latest --example nextjs-blocknote --api-key
+```
+
+
+
+You can also try the same example in a [live demo](https://liveblocks.io/examples/collaborative-text-editor/nextjs-blocknote).
+
+For a simpler demo, check out [this example](/examples/collaboration/liveblocks), which follows their [getting started guide](https://liveblocks.io/docs/get-started/react-blocknote).
+
+If you want more info on integrating Liveblocks, take a look at their [ready-made features for BlockNote](https://liveblocks.io/docs/ready-made-features/text-editor/blocknote) and [API reference](https://liveblocks.io/docs/api-reference/liveblocks-react-blocknote#AnchoredThreads).
+
+## Partykit
+
+For development purposes, you can use our Partykit server to test collaborative features. Replace the `WebrtcProvider` provider in the example below with a `YPartyKitProvider`:
+
+```typescript
+// npm install y-partykit
+import YPartyKitProvider from "y-partykit/provider";
+
+const provider = new YPartyKitProvider(
+ "blocknote-dev.yousefed.partykit.dev",
+ // use a unique name as a "room" for your application:
+ "your-project-name",
+ doc,
+);
+```
+
+To learn how to set up your own development / production servers, check out the [PartyKit docs](https://github.com/partykit/partykit) and the [BlockNote + Partykit example](https://github.com/partykit/partykit/tree/main/examples/blocknote).
diff --git a/docs/content/docs/features/collaboration/meta.json b/docs/content/docs/features/collaboration/meta.json
new file mode 100644
index 0000000000..4815040c88
--- /dev/null
+++ b/docs/content/docs/features/collaboration/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Collaboration",
+ "pages": ["comments"]
+}
diff --git a/docs/content/docs/features/custom-schemas/custom-blocks.mdx b/docs/content/docs/features/custom-schemas/custom-blocks.mdx
new file mode 100644
index 0000000000..60bacafe68
--- /dev/null
+++ b/docs/content/docs/features/custom-schemas/custom-blocks.mdx
@@ -0,0 +1,246 @@
+---
+title: Custom Blocks
+description: Learn how to create custom block types for your BlockNote editor
+---
+
+# Custom Block Types
+
+In addition to the default block types that BlockNote offers, you can also make your own custom blocks using React components. Take a look at the demo below, in which we add a custom alert block to a BlockNote editor, as well as a custom [Slash Menu Item](/docs/react/components/suggestion-menus#changing-slash-menu-items) to insert it.
+
+
+
+## Creating a Custom Block Type
+
+Use the `createReactBlockSpec` function to create a custom block type. This function takes three arguments:
+
+```typescript
+function createReactBlockSpec(
+ blockConfig: CustomBlockConfig,
+ blockImplementation: ReactCustomBlockImplementation,
+ extensions?: BlockNoteExtension[],
+): (options? BlockOptions) => BlockSpec;
+```
+
+It returns a function that you can call to create an instance of your custom block, or a `BlockSpec`. This `BlockSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the block to the editor. This function may also take arbitrary options, which you can find out more about [below](/docs/features/custom-schemas/custom-blocks#block-config-options).
+
+Let's look at our custom alert block from the demo, and go over everything we pass to `createReactBlockSpec`:
+
+```typescript
+const createAlert = createReactBlockSpec(
+ {
+ type: "alert",
+ propSchema: {
+ textAlignment: defaultProps.textAlignment,
+ textColor: defaultProps.textColor,
+ type: {
+ default: "warning",
+ values: ["warning", "error", "info", "success"],
+ },
+ },
+ content: "inline",
+ },
+ {
+ render: (props) => {
+ ...
+ },
+ }
+);
+```
+
+### Block Config (`CustomBlockConfig`)
+
+The Block Config describes the shape of your custom blocks. Use it to specify the type, properties (props) and content your custom blocks should support:
+
+```typescript
+type BlockConfig = {
+ type: string;
+ content: "inline" | "none";
+ readonly propSchema: PropSchema;
+};
+```
+
+`type:` Defines the identifier of the custom block.
+
+`content:` `inline` if your custom block should support rich text content, `none` if not.
+
+
+ _In the alert demo, we want the user to be able to type text in our alert, so
+ we set `content` to `"inline"`._
+
+
+`propSchema:` The `PropSchema` specifies the props that the block supports. Block props (properties) are data stored with your Block in the document, and can be used to customize its appearance or behavior.
+
+```typescript
+type PropSchema = Record<
+ string,
+ | {
+ default: PrimitiveType;
+ values?: PrimitiveType[];
+ }
+ | {
+ default: undefined;
+ type: PrimitiveType;
+ values?: PrimitiveType[];
+ }
+>;
+```
+
+`[key: string]` is the name of the prop. If you want it to have a default value, it should be defined as an object with the following properties:
+
+- `default:` Specifies the prop's default value, from which `PrimitiveType` is inferred.
+
+- `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`.
+
+If you do not want the prop to have a default value, you can define it as an object with the following properties:
+
+- `default:` Left `undefined`, as there is no default value.
+
+- `type:` Specifies `PrimitiveType` that the prop can be set to, since the default value is `undefined` and cannot be inferred from.
+
+- `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`.
+
+
+ _In the alert demo, we add a `type` prop for the type of alert that we want
+ (warning / error / info / success). We also want basic styling options, so we
+ add text alignment and text color from the [Default Block
+ Properties](/docs/features/blocks#default-block-properties)._
+
+
+### Block Implementation (`ReactCustomBlockImplementation`)
+
+The Block Implementation defines how the block should be rendered in the editor, and how it should be parsed from and converted to HTML.
+
+```typescript
+type ReactCustomBlockImplementation = {
+ render: React.FC<{
+ block: Block;
+ editor: BlockNoteEditor;
+ contentRef?: (node: HTMLElement | null) => void;
+ }>;
+ toExternalHTML?: React.FC<{
+ block: Block;
+ editor: BlockNoteEditor;
+ contentRef?: (node: HTMLElement | null) => void;
+ contest: { nestingLevel: number };
+ }>;
+ parse?: (element: HTMLElement) => PartialBlock["props"] | undefined;
+ runsBefore?: string[];
+ meta?: {
+ hardBreakShortcut?: "shift+enter" | "enter" | "none";
+ selectable?: boolean;
+ fileBlockAccept?: string[];
+ code?: boolean;
+ defining?: boolean;
+ isolating?: boolean;
+ };
+};
+```
+
+`render:` This is your React component which defines how your custom block should be rendered in the editor, and takes three React props:
+
+- `block:` The block that should be rendered. Its type and props will match the type and PropSchema defined in the Block Config.
+
+- `editor:` The BlockNote editor instance that the block is in.
+
+- `contentRef:` A React `ref` you can use to mark which element in your block is editable, this is only available if your block config contains `content: "inline"`.
+
+`toExternalHTML?:` This component is used whenever the block is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render` and an additional `context` prop, which is. an object with the following attributes:
+
+- `nestingLevel`: How deep the block being exported in nested inside other blocks. 0 means it's at the top level of the document.
+
+
+ _Note that your component passed to `toExternalHTML` is rendered and
+ serialized in a separate React root, which means you can't use hooks that rely
+ on React Contexts._
+
+
+`parse?:` The `parse` function defines how to parse HTML content into your block, for example when pasting contents from the clipboard. If the element should be parsed into your custom block, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument:
+
+- `element`: The HTML element that's being parsed.
+
+`runsBefore?:` If this block has parsing or extensions that need to be given priority over any other blocks, you can pass their `type`s in an array here.
+
+`meta?:` An object for setting various generic properties of the block.
+
+- `hardBreakShortcut?:` Defines which keyboard shortcut should be used to insert a hard break into the block's inline content. Defaults to `"shift+enter"`.
+
+- `selectable?:` Can be set to false in order to make the block non-selectable, both using the mouse and keyboard. This also helps with being able to select non-editable content within the block. Should only be set to false when `content` is `none` and defaults to true.
+
+- `fileBlockAccept?:` For custom file blocks, this specifies which MIME types are accepted when uploading a file. All file blocks should specify this property, and should use a [`FileBlockWrapper`](https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/blocks/File/helpers/render/FileBlockWrapper.tsx)/[`ResizableFileBlockWrapper`](https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/blocks/File/helpers/render/ResizableFileBlockWrapper.tsx) component in their `render` functions (see next subsection).
+
+- `code?:` Whether this block contains [code](https://prosemirror.net/docs/ref/#model.NodeSpec.code).
+
+- `defining?:` Whether this block is [defining](https://prosemirror.net/docs/ref/#model.NodeSpec.defining).
+
+- `isolating?:` Whether this block is [isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating).
+
+### Block Extensions
+
+While the example on this page doesn't use it, `createReactBlockSpec` takes a third, optional argument `extensions`. This is for adding editor `extensions` that are specific to the block, which you can find out more about [here](/docs/features/extensions).
+
+Block extensions are typically things like e.g. adding keyboard shortcuts to change the current block type to a custom block. For a table of contents block, an extension could also add a ProseMirror plugin to scan for headings to put in the ToC.
+
+### Block Config Options
+
+In some cases, you may want to have a customizable block config. For example, you may want to be able to have a code block with syntax highlighting for either web or embedded code, or a heading block with a flexible number of heading levels. You can use the same API for this use case, with some minor changes:
+
+```typescript
+// Arbitrary options that your block can take, e.g. number of heading levels or
+// available code syntax highlight languages.
+type CustomBlockConfigOptions = {
+ ...
+}
+
+const createCustomBlock = createReactBlockSpec(
+ createBlockConfig((options: CustomBlockConfigOptions) => ({
+ type: "customBlock"
+ propSchema: ...,
+ content: ...,
+ })),
+ (options: CustomBlockConfigOptions) => ({
+ render: ...,
+ ...
+ })
+)
+
+const options: CustomBlockConfigOptions = {
+ ...
+};
+
+const schema = BlockNoteSchema.create().extend({
+ blockSpecs: {
+ // Creates an instance of the custom block and adds it to the schema.
+ customBlock: createCustomBlock(options),
+ },
+});
+```
+
+You can see that instead of passing plain objects for the config and implementation, we instead pass functions. These take the block options as an argument, and return the config and implementation objects respectively. Additionally, the function for creating the config is wrapped in a `createBlockConfig` function.
+
+Also notice that for the example on this page, we create a new Alert block instance by simply calling `createAlert()` with no arguments. When a custom block takes options though, you can pass them in when creating an instance, as shown above.
+
+To see a full example of block options being used, check out the [built-in heading block](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Heading/block.ts).
+
+## Adding Custom Blocks to the Editor
+
+Finally, create a BlockNoteSchema using the definition of your custom blocks:
+
+```typescript
+const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ // enable the default blocks if desired
+ ...defaultBlockSpecs,
+
+ // Add your own custom blocks:
+ alert: createAlert(),
+ },
+});
+```
+
+You can then instantiate your editor with this custom schema, as explained on the [Custom Schemas](/docs/features/custom-schemas) page.
+
+## Improving the User Experience
+
+Now you know how to create a custom block and add it to your editor. However, users don't have a way of creating instances of it to their documents.
+
+To fix this, it's recommended to implement a [command to insert your custom in the Slash Menu](/docs/react/components/suggestion-menus#changing-slash-menu-items), and [an item for it in the Block Type Select](/docs/react/components/formatting-toolbar)
diff --git a/docs/content/docs/features/custom-schemas/custom-inline-content.mdx b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx
new file mode 100644
index 0000000000..efc8c424ae
--- /dev/null
+++ b/docs/content/docs/features/custom-schemas/custom-inline-content.mdx
@@ -0,0 +1,172 @@
+---
+title: Custom Inline Content
+description: Learn how to create custom inline content for your BlockNote editor
+---
+
+# Custom Inline Content Types
+
+In addition to the default inline content types that BlockNote offers, you can also make your own custom inline content using React components. Take a look at the demo below, in which we add a custom mention tag to a BlockNote editor, as well as a custom [Suggestion Menu](/docs/react/components/suggestion-menus) to insert it.
+
+
+
+## Creating a Custom Inline Content Type
+
+Use the `createReactInlineContentSpec` function to create a custom inline content type. This function takes two arguments:
+
+```typescript
+function createReactInlineContentSpec(
+ blockConfig: CustomInlineContentConfig,
+ blockImplementation: ReactInlineContentImplementation,
+): InlineContentSpec;
+```
+
+It returns an instance of your custom inline content, or an `InlineContentSpec`. This `InlineContentSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the inline content to the editor.
+
+Let's look at our custom mentions tag from the demo, and go over everything we pass to `createReactInlineContentSpec`:
+
+```typescript
+const Mention = createReactInlineContentSpec(
+ {
+ type: "mention",
+ propSchema: {
+ user: {
+ default: "Unknown",
+ },
+ },
+ content: "none",
+ } as const,
+ {
+ render: (props) => (
+ ...
+ ),
+ }
+);
+```
+
+### Inline Content Config (`CustomInlineContentConfig`)
+
+The Inline Content Config describes the shape of your custom inline content. Use it to specify the type, properties (props) and content your custom inline content should support:
+
+```typescript
+type CustomInlineContentConfig = {
+ type: string;
+ content: "styled" | "none";
+ readonly propSchema: PropSchema;
+};
+```
+
+`type:` Defines the identifier of the custom inline content.
+
+`content:` `styled` if your custom inline content should contain [`StyledText`](/docs/foundations/document-structure#inline-content-objects), `none` if not.
+
+
+ _In the mentions demo, we want each mention to be a single, non-editable
+ element, so we set `content` to `"none"`._
+
+
+`propSchema:` The `PropSchema` specifies the props that the inline content supports. Inline content props (properties) are data stored with your inline content in the document, and can be used to customize its appearance or behavior.
+
+```typescript
+type PropSchema = Record<
+ string,
+ | {
+ default: PrimitiveType;
+ values?: PrimitiveType[];
+ }
+ | {
+ default: undefined;
+ type: PrimitiveType;
+ values?: PrimitiveType[];
+ }
+>;
+```
+
+`[key: string]` is the name of the prop. If you want it to have a default value, it should be defined as an object with the following properties:
+
+- `default:` Specifies the prop's default value, from which `PrimitiveType` is inferred.
+
+- `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`.
+
+If you do not want the prop to have a default value, you can define it as an object with the following properties:
+
+- `default:` Left `undefined`, as there is no default value.
+
+- `type:` Specifies `PrimitiveType` that the prop can be set to, since the default value is `undefined` and cannot be inferred from.
+
+- `values?:` Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. If `values` is not defined, BlockNote assumes the prop can be any value of `PrimitiveType`.
+
+
+ _In the mentions demo, we add a `user` prop for the user that's being
+ mentioned._
+
+
+### Inline Content Implementation (`ReactCustomInlineContentImplementation`)
+
+The Inline Content Implementation defines how the inline content should be rendered to HTML.
+
+```typescript
+type ReactCustomInlineContentImplementation = {
+ meta?: {
+ draggable?: boolean;
+ };
+ render: React.FC<{
+ inlineContent: InlineContent;
+ editor: BlockNoteEditor;
+ contentRef?: (node: HTMLElement | null) => void;
+ }>;
+ toExternalHTML?: React.FC<{
+ inlineContent: InlineContent;
+ editor: BlockNoteEditor;
+ contentRef?: (node: HTMLElement | null) => void;
+ }>;
+ parse?: (element: HTMLElement) => PartialInlineContent["props"] | undefined;
+ meta?: {
+ draggable?: boolean;
+ };
+};
+```
+
+`render:` This is your React component which defines how your custom inline content should be rendered, and takes three React props:
+
+- `inlineContent:` The inline content that should be rendered. Its type and props will match the type and PropSchema defined in the Inline Content Config.
+
+- `contentRef:` A React `ref` you can use to mark which element in your inline content is editable, this is only available if your inline content config contains `content: "styled"`.
+
+- `draggable:` Specifies whether the inline content can be dragged within the editor. If set to `true`, the inline content will be draggable. Defaults to `false` if not specified. If this is true, you should add `data-drag-handle` to the DOM element that should function as the drag handle.
+
+`toExternalHTML?:` This component is used whenever the inline content is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`.
+
+
+ _Note that your component passed to `toExternalHTML` is rendered and
+ serialized in a separate React root, which means you can't use hooks that rely
+ on React Contexts._
+
+
+`parse?:` The `parse` function defines how to parse HTML content into your inline content, for example when pasting contents from the clipboard. If the element should be parsed into your custom inline content, you return the props that the block should be given. Otherwise, return `undefined`. Takes a single argument:
+
+- `element`: The HTML element that's being parsed.
+
+`meta?.draggable?:` Whether the inline content should be draggable.
+
+
+ _Note that since inline content is, by definition, inline, your component
+ should also return an HTML inline element._
+
+
+## Adding Custom Inline Content to the Editor
+
+Finally, create a BlockNoteSchema using the definition of your custom inline content:
+
+```typescript
+const schema = BlockNoteSchema.create({
+ inlineContentSpecs: {
+ // enable the default inline content if desired
+ ...defaultInlineContentSpecs,
+
+ // Add your own custom inline content:
+ mention: Mention,
+ },
+});
+```
+
+You can then instantiate your editor with this custom schema, as explained on the [Custom Schemas](/docs/features/custom-schemas) page.
diff --git a/docs/content/docs/features/custom-schemas/custom-styles.mdx b/docs/content/docs/features/custom-schemas/custom-styles.mdx
new file mode 100644
index 0000000000..6339ff9f37
--- /dev/null
+++ b/docs/content/docs/features/custom-schemas/custom-styles.mdx
@@ -0,0 +1,113 @@
+---
+title: Custom Styles
+description: Learn how to create custom style schemas for your BlockNote editor
+---
+
+# Custom Style Types
+
+In addition to the default style types that BlockNote offers, you can also make your own custom styles using React components. Take a look at the demo below, in which we add a custom font style to a BlockNote editor, as well as a custom [Formatting Toolbar button](/docs/react/components/formatting-toolbar) to set it.
+
+
+
+## Creating a Custom Style Type
+
+Use the `createReactStyleSpec` function to create a custom style type. This function takes two arguments:
+
+```typescript
+function createReactStyleSpec(
+ styleConfig: CustomStyleConfig,
+ styleImplementation: ReactStyleImplementation,
+): StyleSpec;
+```
+
+It returns an instance of your custom inline content, or a `StyleSpec`. This `StyleSpec` then gets passed into your [BlockNote schema](/docs/features/custom-schemas#creating-your-own-schema) to add the style to the editor.
+
+Let's look at our custom font style from the demo, and go over everything we pass to `createReactStyleSpec`:
+
+```typescript
+export const Font = createReactStyleSpec(
+ {
+ type: "font",
+ propSchema: "string",
+ },
+ {
+ render: (props) => (
+
+ ),
+ }
+);
+```
+
+### Style Config (`CustomStyleConfig`)
+
+The Style Config describes the shape of your custom style. Use it to specify the type, and whether the style should take a string value:
+
+```typescript
+type CustomStyleConfig = {
+ type: string;
+ readonly propSchema: "boolean" | "string";
+};
+```
+
+`type:` Defines the identifier of the custom style.
+
+`propSchema:` The `PropSchema` specifies whether the style can only be toggled (`"boolean"`), or whether it can take a string value (`"string"`). Having a string value is useful for e.g. setting a color on the style.
+
+
+ _In the font style demo, we set `propSchema` to `"string"` so we can store the
+ font family._
+
+
+### Style Implementation (`ReactCustomStyleImplementation`)
+
+The Style Implementation defines how the style should be rendered to HTML.
+
+```typescript
+type ReactCustomStyleImplementation = {
+ render: React.FC<{
+ value?: string;
+ contentRef: (node: HTMLElement | null) => void;
+ }>;
+ toExternalHTML?: React.FC<{
+ value?: string;
+ contentRef: (node: HTMLElement | null) => void;
+ }>;
+ parse?: (element: HTMLElement) => string | true | undefined;
+};
+```
+
+`render:` This is your React component which defines how your custom style should be rendered, and takes two React props:
+
+- `value:` The string value of the style, this is only available if your style config contains `propSchema: "string"`.
+
+- `contentRef:` A React `ref` to mark the editable element.
+
+`toExternalHTML?:` This component is used whenever the style is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render`.
+
+
+ _Note that your component passed to `toExternalHTML` is rendered and
+ serialized in a separate React root, which means you can't use hooks that rely
+ on React Contexts._
+
+
+`parse?:` The `parse` function defines how to parse HTML content into your style, for example when pasting contents from the clipboard. If the element should be parsed into your custom style, you return a `string` or `true`. If the `propSchema` is `"string"`, you should likewise return a string value, or `true` otherwise. Returning `undefined` will not parse the style from the HTML element. Takes a single argument:
+
+- `element`: The HTML element that's being parsed.
+
+## Adding Custom Style to the Editor
+
+Finally, create a BlockNoteSchema using the definition of your custom style:
+
+```typescript
+const schema = BlockNoteSchema.create({
+ styleSpecs: {
+ // enable the default styles if desired
+ ...defaultStyleSpecs,
+
+ // Add your own custom style:
+ font: Font,
+ },
+});
+```
+
+You can then instantiate your editor with this custom schema, as explained on the [Custom Schemas](/docs/features/custom-schemas) page.
diff --git a/docs/content/docs/features/custom-schemas/index.mdx b/docs/content/docs/features/custom-schemas/index.mdx
new file mode 100644
index 0000000000..ae2dd24754
--- /dev/null
+++ b/docs/content/docs/features/custom-schemas/index.mdx
@@ -0,0 +1,138 @@
+---
+title: Custom Schemas
+description: Learn how to create custom schemas for your BlockNote editor
+---
+
+# Custom Schemas
+
+By default, BlockNote documents support different kind of blocks, inline content and text styles (see [default schema](/docs/foundations/schemas)).
+However, you can extend BlockNote and create custom schemas to support your own blocks, inline content and text styles.
+
+## Custom Blocks
+
+Blocks are the main elements of a document, such as paragraphs, headings, lists, etc.
+
+- [Learn how to create custom blocks for your BlockNote editor](/docs/features/custom-schemas/custom-blocks)
+
+## Custom Inline Content
+
+Inline Content are elements that can be inserted inside a text block, such as links, mentions, tags, etc.
+
+- [Learn how to create custom Inline Content for your BlockNote editor](/docs/features/custom-schemas/custom-inline-content)
+
+## Custom Styles
+
+Text Styles are properties that can be applied to a piece of text, such as bold, italic, underline, etc.
+
+- [Learn how to add custom Styles to your BlockNote editor](/docs/features/custom-schemas/custom-styles)
+
+## Creating your own schema
+
+Once you have defined your custom blocks (see the links above), inline content or styles, you can create a schema and pass this to the initialization of the editor. There are two ways to create a new schema.
+
+### Extending an existing schema
+
+You can call `BlockNoteSchema.extend` to add custom blocks, inline content, or styles to an existing schema. While this works for any existing schema, it's most common to use this to extend the default schema.
+
+```typescript
+// Creates an instance of the default schema when nothing is passed to
+// `BlockNoteSchema.create`.
+const schema = BlockNoteSchema.create()
+ // Adds custom blocks, inline content, or styles to the default schema.
+ .extend({
+ blockSpecs: {
+ // Add your own custom blocks:
+ customBlock: CustomBlock,
+ ...
+ },
+ inlineContentSpecs: {
+ // Add your own custom inline content:
+ customInlineContent: CustomInlineContent,
+ ...
+ },
+ styleSpecs: {
+ // Add your own custom styles:
+ customStyle: CustomStyle,
+ ...
+ },
+ });
+```
+
+### Creating a schema from scratch
+
+Passing custom blocks, inline content, or styles directly into `BlockNoteSchema.create` will produce a new schema with only the things you pass. This can be useful if you only need a few basic things from the default schema, and intend to implement everything else yourself.
+
+```typescript
+const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ // Add only the default paragraph block:
+ paragraph: defaultBlockSpecs.paragraph,
+
+ // Add your own custom blocks:
+ customBlock: CustomBlock,
+ ...
+ },
+ inlineContentSpecs: {
+ // Add only the default text inline content:
+ text: defaultInlineContentSpecs.text,
+
+ // Add your own custom inline content:
+ customInlineContent: CustomInlineContent,
+ ...
+ },
+ styleSpecs: {
+ // Add only the default bold style:
+ bold: defaultStyleSpecs.bold,
+
+ // Add your own custom styles:
+ customStyle: CustomStyle,
+ ...
+ },
+});
+```
+
+## Using your own schema
+
+Once you've created an instance of your schema using `BlockNoteSchema.create` or `BlockNoteSchema.extend`, you can pass it to the `schema` option of your BlockNoteEditor (`BlockNoteEditor.create` or `useCreateBlockNote`):
+
+```typescript
+const editor = useCreateBlockNote({
+ schema,
+});
+```
+
+## Usage with TypeScript
+
+In contrast to most other editors, BlockNote has been designed for full TypeScript compatibility. This means you can get full type safety and autocompletion _even when using a custom schema_.
+
+By default, the methods, hooks, and types exposed by the API assume you're using the default, built-in schema. If you're using a custom schema, there are 3 ways to get full type safety:
+
+### Methods that accept an optional `schema` parameter
+
+Some methods, like the `useBlockNoteEditor` hook, take an optional `schema?: BlockNoteSchema` parameter. If you're using a custom schema, you should pass it here to make sure the return type is correctly typed.
+
+### Manual typing of types
+
+If you're using types like `BlockNoteEditor`, `Block`, `PartialBlock` directly, you can get the correctly typed variants like this:
+
+```typescript
+type MyBlock = Block<
+ typeof schema.blockSchema,
+ typeof schema.inlineContentSchema,
+ typeof schema.styleSchema
+>;
+```
+
+Or even simpler, use the shorthands exposed by the schema:
+
+```typescript
+type MyBlockNoteEditor = typeof schema.BlockNoteEditor;
+type MyBlock = typeof schema.Block;
+type MyPartialBlock = typeof schema.PartialBlock;
+```
+
+### Automatically override all default types (experimental)
+
+Alternatively, the easiest way to get full type safety without any additional work is to override all default types with your custom schema, by using a custom type definition file. See this [example blocknote.d.ts](https://github.com/TypeCellOS/BlockNote/blob/main/examples/06-custom-schema/react-custom-styles/blocknote.d.ts.example). This is an experimental feature - we would love to hear your feedback on this approach.
+
+
diff --git a/docs/content/docs/features/export/docx.mdx b/docs/content/docs/features/export/docx.mdx
new file mode 100644
index 0000000000..2536daa6bb
--- /dev/null
+++ b/docs/content/docs/features/export/docx.mdx
@@ -0,0 +1,126 @@
+---
+title: DOCX
+description: Export BlockNote documents to a docx word (Office Open XML) file.
+imageTitle: DOCX Export
+path: /docs/export-to-docx
+---
+
+# DOCX Export
+
+It's possible to export BlockNote documents to docx, completely client-side.
+
+
+ This feature is provided by the `@blocknote/xl-docx-exporter`. `xl-` packages
+ are fully open source, but released under a copyleft license. A commercial
+ license for usage in closed source, proprietary products comes as part of the
+ [Business subscription](/pricing).
+
+
+First, install the `@blocknote/xl-docx-exporter` and `docx` packages:
+
+```bash
+npm install @blocknote/xl-docx-exporter docx
+```
+
+Then, create an instance of the `DOCXExporter` class. This exposes the following methods:
+
+```typescript
+import {
+ DOCXExporter,
+ docxDefaultSchemaMappings,
+} from "@blocknote/xl-docx-exporter";
+import { Packer } from "docx";
+
+// Create the exporter
+const exporter = new DOCXExporter(editor.schema, docxDefaultSchemaMappings);
+
+// Convert the blocks to a docxjs document
+const docxDocument = await exporter.toDocxJsDocument(editor.document);
+
+// Use docx to write to file:
+await Packer.toBuffer(docxDocument);
+```
+
+See the [full example](/examples/interoperability/converting-blocks-to-docx) below:
+
+
+
+### Customizing the Docx output file
+
+`toDocxJsDocument` takes an optional `options` parameter, which allows you to customize document metadata (like the author) and section options (like headers and footers).
+
+Example usage:
+
+```typescript
+import { Paragraph, TextRun } from "docx";
+
+const doc = await exporter.toDocxJsDocument(testDocument, {
+ documentOptions: {
+ creator: "John Doe",
+ },
+ sectionOptions: {
+ headers: {
+ default: {
+ options: {
+ children: [new Paragraph({ children: [new TextRun("Header")] })],
+ },
+ },
+ },
+ footers: {
+ default: {
+ options: {
+ children: [new Paragraph({ children: [new TextRun("Footer")] })],
+ },
+ },
+ },
+ },
+});
+```
+
+### Custom mappings / custom schemas
+
+The `DOCXExporter` constructor takes a `schema`, `mappings` and `options` parameter.
+A _mapping_ defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) to a [docxjs](https://docx.js.org/) element.
+If you're using a [custom schema](/docs/features/custom-schemas) in your editor, or if you want to overwrite how default BlockNote elements are converted to docx, you can pass your own `mappings`:
+
+For example, use the following code in case your schema has an `extraBlock` type:
+
+```typescript
+import {
+ DOCXExporter,
+ docxDefaultSchemaMappings,
+} from "@blocknote/xl-docx-exporter";
+import { Paragraph, TextRun } from "docx";
+
+new DOCXExporter(schema, {
+ blockMapping: {
+ ...docxDefaultSchemaMappings.blockMapping,
+ myCustomBlock: (block, exporter) => {
+ return new Paragraph({
+ children: [
+ new TextRun({
+ text: "My custom block",
+ }),
+ ],
+ });
+ },
+ },
+ inlineContentMapping: docxDefaultSchemaMappings.inlineContentMapping,
+ styleMapping: docxDefaultSchemaMappings.styleMapping,
+});
+```
+
+### Exporter options
+
+The `DOCXExporter` constructor takes an optional `options` parameter.
+While conversion happens on the client-side, the default setup uses a server hosted proxy to resolve files:
+
+```typescript
+const defaultOptions = {
+ // a function to resolve external resources in order to avoid CORS issues
+ // by default, this calls a BlockNote hosted server-side proxy to resolve files
+ resolveFileUrl: corsProxyResolveFileUrl,
+ // the colors to use in the Docx for things like highlighting, background colors and font colors.
+ colors: COLORS_DEFAULT, // defaults from @blocknote/core
+};
+```
diff --git a/docs/content/docs/features/export/email.mdx b/docs/content/docs/features/export/email.mdx
new file mode 100644
index 0000000000..b36f7ccd58
--- /dev/null
+++ b/docs/content/docs/features/export/email.mdx
@@ -0,0 +1,170 @@
+---
+title: Email Export
+description: Export BlockNote documents to an email using React Email.
+---
+
+# Email Export
+
+It's possible to export BlockNote documents to email-compatible HTML, completely client-side.
+
+
+ This feature is provided by the `@blocknote/xl-email-exporter`. `xl-` packages
+ are fully open source, but released under a copyleft license. A commercial
+ license for usage in closed source, proprietary products comes as part of the
+ [Business subscription](/pricing).
+
+
+First, install the `@blocknote/xl-email-exporter` packages:
+
+```bash
+npm install @blocknote/xl-email-exporter
+```
+
+Then, create an instance of the `ReactEmailExporter` class. This exposes the following methods:
+
+```typescript
+import {
+ ReactEmailExporter,
+ reactEmailDefaultSchemaMappings,
+} from "@blocknote/xl-email-exporter";
+
+// Create the exporter
+const exporter = new ReactEmailExporter(
+ editor.schema,
+ reactEmailDefaultSchemaMappings,
+);
+
+// Convert the blocks to a react-email document
+const html = await exporter.toReactEmailDocument(editor.document);
+
+// Use react-email to write to file:
+await ReactEmail.render(html, `filename.html`);
+```
+
+See the [full example](/examples/interoperability/converting-blocks-to-react-email) below:
+
+
+
+### Customizing the Email output
+
+`toReactEmailDocument` takes an optional `options` parameter, which allows you to customize:
+
+- **preview**: Set the preview text for the email (can be a string or an array of strings)
+- **header**: Add content to the top of the email (must be a React-Email compatible component)
+- **footer**: Add content to the bottom of the email (must be a React-Email compatible component)
+- **head**: Inject elements into the [Head element](https://react.email/docs/components/head)
+- **container**: Customize the container element (A component which will wrap the email content including the header and footer)
+- **bodyStyles**: Customize the body styles (a `CSSProperties` object), providing an object here will completely override the default styles with what you provide
+
+Example usage:
+
+```tsx
+import React from "react";
+import {
+ ReactEmailExporter,
+ reactEmailDefaultSchemaMappings,
+} from "@blocknote/xl-email-exporter";
+import { BlockNoteEditor } from "@blocknote/core";
+import { Text, Container } from "@react-email/components";
+
+const editor = BlockNoteEditor.create();
+
+// ---cut---
+const exporter = new ReactEmailExporter(
+ editor.schema,
+ reactEmailDefaultSchemaMappings,
+);
+
+const html = await exporter.toReactEmailDocument(editor.document, {
+ preview: "This is a preview of the email content",
+ header: Header,
+ footer: Footer,
+ head: My email,
+ container: ({ children }) => {children},
+ // These are the default body styles that are set by default
+ bodyStyles: {
+ fontFamily:
+ "'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif",
+ fontSize: "16px",
+ lineHeight: "1.5",
+ color: "#333",
+ },
+});
+```
+
+### Custom mappings / custom schemas
+
+The `ReactEmailExporter` constructor takes a `schema` and `mappings` parameter.
+A _mapping_ defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) to a React-Email element.
+If you're using a [custom schema](/docs/features/custom-schemas) in your editor, or if you want to overwrite how default BlockNote elements are converted to React Email, you can pass your own `mappings`:
+
+For example, use the following code in case your schema has an `extraBlock` type:
+
+```typescript
+import { ReactEmailExporter, reactEmailDefaultSchemaMappings } from "@blocknote/xl-email-exporter";
+import { Text } from "@react-email/components";
+
+new ReactEmailExporter(schema, {
+ blockMapping: {
+ ...reactEmailDefaultSchemaMappings.blockMapping,
+ myCustomBlock: (block, exporter) => {
+ return My custom block;
+ },
+ },
+ inlineContentMapping: reactEmailDefaultSchemaMappings.inlineContentMapping,
+ styleMapping: reactEmailDefaultSchemaMappings.styleMapping,
+});
+```
+
+### Exporter options
+
+The `ReactEmailExporter` constructor takes an optional `options` parameter.
+While conversion happens on the client-side, the default setup uses a server hosted proxy to resolve files:
+
+```typescript
+const defaultOptions = {
+ // a function to resolve external resources in order to avoid CORS issues
+ // by default, this calls a BlockNote hosted server-side proxy to resolve files
+ resolveFileUrl: corsProxyResolveFileUrl,
+ // the colors to use in the email for things like highlighting, background colors and font colors.
+ colors: COLORS_DEFAULT, // defaults from @blocknote/core
+};
+```
+
+### Custom styles
+
+Want to tweak the default styles of the email? You can use `reactEmailDefaultSchemaMappingsWithStyles` to create a custom mapping with your own styles.
+
+```tsx
+import {
+ ReactEmailExporter,
+ reactEmailDefaultSchemaMappingsWithStyles,
+} from "@blocknote/xl-email-exporter";
+import { Text } from "@react-email/components";
+
+const { blockMapping, inlineContentMapping, styleMapping } =
+ reactEmailDefaultSchemaMappingsWithStyles({
+ textStyles: {
+ paragraph: {
+ style: {
+ fontSize: 16,
+ lineHeight: 1.5,
+ margin: 3,
+ minHeight: 24,
+ },
+ },
+ },
+ });
+
+new ReactEmailExporter(schema, {
+ // You can still use the default block mapping, but you can also overwrite it
+ blockMapping: {
+ ...blockMapping,
+ audio: (block, exporter) => {
+ return Audio block;
+ },
+ },
+ inlineContentMapping,
+ styleMapping,
+});
+```
diff --git a/docs/content/docs/features/export/html.mdx b/docs/content/docs/features/export/html.mdx
new file mode 100644
index 0000000000..25410b5d90
--- /dev/null
+++ b/docs/content/docs/features/export/html.mdx
@@ -0,0 +1,62 @@
+---
+title: HTML
+description: It's possible to export Blocks to HTML, completely client-side.
+imageTitle: HTML Export
+path: /docs/converting-blocks
+---
+
+# HTML Export
+
+
+ The functions to export to HTML are considered "lossy"; some information might be dropped when you export Blocks to HTML.
+
+ To serialize Blocks to a non-lossy format (for example, to store the contents of the editor in your backend), simply export the built-in Block format using `JSON.stringify(editor.document)`.
+
+
+
+## Export to BlockNote HTML
+
+Use `editor.blocksToFullHTML` to export blocks with their full HTML structure, the same as BlockNote uses in its rendered HTML.
+
+For example, you an use this for static rendering documents that have been created in the editor.
+
+
+ For the exported HTML to look the same as the editor, make sure to wrap it in the same `div`s that the editor renders, and add the same stylesheets. To learn more, see [this example](/examples/backend/rendering-static-documents).
+
+
+
+```typescript
+blocksToFullHTML(blocks?: Block[]): string;
+
+// Usage
+const HTMLFromBlocks = editor.blocksToFullHTML(blocks);
+```
+
+`blocks:` The blocks to convert. If not provided, the entire document (all top-level blocks) is used.
+
+`returns:` The blocks, exported to an HTML string.
+
+## Export to Interoperable HTML
+
+The editor exposes functions to convert Blocks to and from HTML for interoperability with other applications.
+
+Converting Blocks to HTML this way will lose some information such as the nesting of nodes in order to export a simple HTML structure.
+
+Use `blocksToHTMLLossy` to export `Block` objects to an HTML string:
+
+```typescript
+blocksToHTMLLossy(blocks?: Block[]): string;
+
+// Usage
+const HTMLFromBlocks = editor.blocksToHTMLLossy(blocks);
+```
+
+`blocks:` The blocks to convert. If not provided, the entire document (all top-level blocks) is used.
+
+`returns:` The blocks, exported to an HTML string.
+
+To better conform to HTML standards, children of blocks which aren't list items are un-nested in the output HTML.
+
+**Demo**
+
+
diff --git a/docs/content/docs/features/export/markdown.mdx b/docs/content/docs/features/export/markdown.mdx
new file mode 100644
index 0000000000..b151728ea1
--- /dev/null
+++ b/docs/content/docs/features/export/markdown.mdx
@@ -0,0 +1,38 @@
+---
+title: Markdown
+description: It's possible to export Blocks to Markdown, completely client-side.
+imageTitle: Markdown Export
+path: /docs/converting-blocks
+---
+
+# Markdown Export
+
+
+ The functions to export to Markdown are considered "lossy"; some information might be dropped when you export Blocks to Markdown.
+
+ To serialize Blocks to a non-lossy format (for example, to store the contents of the editor in your backend), simply export the built-in Block format using `JSON.stringify(editor.document)`.
+
+
+
+BlockNote can export Blocks to Markdown. Note that this is also considered "lossy", as not all structures can be entirely represented in Markdown.
+
+### Export Markdown
+
+`blocksToMarkdownLossy` converts `Block` objects to a Markdown string:
+
+```typescript
+blocksToMarkdownLossy(blocks?: Block[]): string;
+
+// Usage
+const markdownFromBlocks = editor.blocksToMarkdownLossy(blocks);
+```
+
+`blocks:` The blocks to convert. If not provided, the entire document (all top-level blocks) is used.
+
+`returns:` The blocks, serialized as a Markdown string.
+
+The output is simplified as Markdown does not support all features of BlockNote (e.g.: children of blocks which aren't list items are un-nested and certain styles are removed).
+
+**Demo**
+
+
diff --git a/docs/content/docs/features/export/meta.json b/docs/content/docs/features/export/meta.json
new file mode 100644
index 0000000000..eba425dcaf
--- /dev/null
+++ b/docs/content/docs/features/export/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Export",
+ "pages": ["markdown", "html", "pdf", "docx", "email", "odt", "..."]
+}
diff --git a/docs/content/docs/features/export/odt.mdx b/docs/content/docs/features/export/odt.mdx
new file mode 100644
index 0000000000..3f5248a875
--- /dev/null
+++ b/docs/content/docs/features/export/odt.mdx
@@ -0,0 +1,101 @@
+---
+title: ODT
+description: Export BlockNote documents to an ODT (Open Document Text) file.
+imageTitle: ODT Export
+path: /docs/export-to-odt
+---
+
+# ODT Export
+
+It's possible to export BlockNote documents to ODT, completely client-side.
+
+
+ This feature is provided by the `@blocknote/xl-odt-exporter`. `xl-` packages
+ are fully open source, but released under a copyleft license. A commercial
+ license for usage in closed source, proprietary products comes as part of the
+ [Business subscription](/pricing).
+
+
+First, install the `@blocknote/xl-odt-exporter` package:
+
+```bash
+npm install @blocknote/xl-odt-exporter
+```
+
+Then, create an instance of the `ODTExporter` class. This exposes the following methods:
+
+```typescript
+import {
+ ODTExporter,
+ odtDefaultSchemaMappings,
+} from "@blocknote/xl-odt-exporter";
+
+// Create the exporter
+const exporter = new ODTExporter(editor.schema, odtDefaultSchemaMappings);
+
+// Convert the blocks to a ODT document (Blob)
+const odtDocument = await exporter.toODTDocument(editor.document);
+```
+
+See the [full example](/examples/interoperability/converting-blocks-to-odt) below:
+
+
+
+### Customizing the ODT output file
+
+`toODTDocument` takes an optional `options` parameter, which allows you to customize different options (like headers and footers).
+
+Example usage:
+
+```typescript
+const odt = await exporter.toODTDocument(testDocument, {
+ // XML string
+ footer: "FOOTER",
+ // XMLDocument
+ header: new DOMParser().parseFromString(
+ `HEADER`,
+ "text/xml",
+ ),
+});
+```
+
+### Custom mappings / custom schemas
+
+The `ODTExporter` constructor takes a `schema`, `mappings` and `options` parameter.
+A _mapping_ defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) to the [ODT](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part3-schema/OpenDocument-v1.3-os-part3-schema.html) XML element.
+If you're using a [custom schema](/docs/features/custom-schemas) in your editor, or if you want to overwrite how default BlockNote elements are converted to ODT XML elements, you can pass your own `mappings`:
+
+For example, use the following code in case your schema has an `extraBlock` type:
+
+```tsx
+import {
+ ODTExporter,
+ odtDefaultSchemaMappings,
+} from "@blocknote/xl-odt-exporter";
+
+new ODTExporter(schema, {
+ blockMapping: {
+ ...odtDefaultSchemaMappings.blockMapping,
+ myCustomBlock: (block, exporter) => {
+ return My custom block;
+ },
+ },
+ inlineContentMapping: odtDefaultSchemaMappings.inlineContentMapping,
+ styleMapping: odtDefaultSchemaMappings.styleMapping,
+});
+```
+
+### Exporter options
+
+The `ODTExporter` constructor takes an optional `options` parameter.
+While conversion happens on the client-side, the default setup uses a server hosted proxy to resolve files:
+
+```typescript
+const defaultOptions = {
+ // a function to resolve external resources in order to avoid CORS issues
+ // by default, this calls a BlockNote hosted server-side proxy to resolve files
+ resolveFileUrl: corsProxyResolveFileUrl,
+ // the colors to use in the ODT for things like highlighting, background colors and font colors.
+ colors: COLORS_DEFAULT, // defaults from @blocknote/core
+};
+```
diff --git a/docs/content/docs/features/export/pdf.mdx b/docs/content/docs/features/export/pdf.mdx
new file mode 100644
index 0000000000..08594cf96f
--- /dev/null
+++ b/docs/content/docs/features/export/pdf.mdx
@@ -0,0 +1,105 @@
+---
+title: PDF
+description: Export BlockNote documents to a PDF.
+imageTitle: PDF Export
+path: /docs/export-to-pdf
+---
+
+# PDF Export
+
+It's possible to export BlockNote documents to PDF, completely client-side.
+
+
+ This feature is provided by the `@blocknote/xl-pdf-exporter`. `xl-` packages
+ are fully open source, but released under a copyleft license. A commercial
+ license for usage in closed source, proprietary products comes as part of the
+ [Business subscription](/pricing).
+
+
+First, install the `@blocknote/xl-pdf-exporter` and `@react-pdf/renderer` packages:
+
+```bash
+npm install @blocknote/xl-pdf-exporter @react-pdf/renderer
+```
+
+Then, create an instance of the `PDFExporter` class. This exposes the following methods:
+
+```typescript
+import {
+ PDFExporter,
+ pdfDefaultSchemaMappings,
+} from "@blocknote/xl-pdf-exporter";
+import * as ReactPDF from "@react-pdf/renderer";
+
+// Create the exporter
+const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings);
+
+// Convert the blocks to a react-pdf document
+const pdfDocument = await exporter.toReactPDFDocument(editor.document);
+
+// Use react-pdf to write to file:
+await ReactPDF.render(pdfDocument, `filename.pdf`);
+```
+
+See the [full example](/examples/interoperability/converting-blocks-to-pdf) with live PDF preview below:
+
+
+
+### Customizing the PDF
+
+`toReactPDFDocument` takes an optional `options` parameter, which allows you to customize the header and footer of the PDF:
+
+Example usage:
+
+```typescript
+import { Text } from "@react-pdf/renderer";
+const pdfDocument = await exporter.toReactPDFDocument(editor.document, {
+ header: Header,
+ footer: Footer,
+});
+```
+
+### Custom mappings / custom schemas
+
+The `PDFExporter` constructor takes a `schema` and `mappings` parameter.
+A _mapping_ defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) to a React-PDF element.
+If you're using a [custom schema](/docs/features/custom-schemas) in your editor, or if you want to overwrite how default BlockNote elements are converted to PDF, you can pass your own `mappings`:
+
+For example, use the following code in case your schema has an `extraBlock` type:
+
+```typescript
+import { PDFExporter, pdfDefaultSchemaMappings } from "@blocknote/xl-pdf-exporter";
+import { Text } from "@react-pdf/renderer";
+
+new PDFExporter(schema, {
+ blockMapping: {
+ ...pdfDefaultSchemaMappings.blockMapping,
+ myCustomBlock: (block, exporter) => {
+ return My custom block;
+ },
+ },
+ inlineContentMapping: pdfDefaultSchemaMappings.inlineContentMapping,
+ styleMapping: pdfDefaultSchemaMappings.styleMapping,
+});
+```
+
+### Exporter options
+
+The `PDFExporter` constructor takes an optional `options` parameter.
+While conversion happens on the client-side, the default setup uses two server based resources:
+
+```typescript
+const defaultOptions = {
+ // emoji source, this is passed to the react-pdf library (https://react-pdf.org/fonts#registeremojisource)
+ // these are loaded from cloudflare + twemoji by default
+ emojiSource: {
+ format: "png",
+ url: "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/",
+ },
+ // a function to resolve external resources in order to avoid CORS issues
+ // by default, this calls a BlockNote hosted server-side proxy to resolve files
+ resolveFileUrl: corsProxyResolveFileUrl,
+ // the colors to use in the PDF for things like highlighting, background colors and font colors.
+ colors: COLORS_DEFAULT, // defaults from @blocknote/core
+};
+```
diff --git a/docs/content/docs/features/extensions.mdx b/docs/content/docs/features/extensions.mdx
new file mode 100644
index 0000000000..0af2d62094
--- /dev/null
+++ b/docs/content/docs/features/extensions.mdx
@@ -0,0 +1,99 @@
+---
+title: Extensions
+description: Add extensions to the editor to add keyboard shortcuts, input rules, and more.
+---
+
+# Extensions
+
+BlockNote includes an extensions system which lets you expand the editor's behaviour. Extensions can include any of the following features:
+
+- Keyboard shortcuts
+- Input rules
+- [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System)
+- [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new)
+
+## Creating an extension
+
+You can create extensions using the `createExtension` function:
+
+```typescript
+type Extension = {
+ key: string;
+ keyboardShortcuts?: Record<
+ string,
+ (ctx: { editor: BlockNoteEditor; }) => boolean
+ >;
+ inputRules?: {
+ find: RegExp;
+ replace: (props: {
+ match: RegExpMatchArray;
+ range: { from: number; to: number };
+ editor: BlockNoteEditor;
+ }) => PartialBlock | undefined;
+ }[];
+ plugins?: Plugin[];
+ tiptapExtensions?: AnyExtension[];
+}
+
+const CustomExtension = createExtension({
+ key: "customBlockExtension",
+ keyboardShortcuts: ...,
+ inputRules: ...,
+ plugins: ...,
+ tiptapExtensions: ...,
+});
+```
+
+Let's go over the options that can be passed into `createExtension`:
+
+`key:` The name of the extension.
+
+`keyboardShortcuts?:` Keyboard shortcuts can be used to run code when a key combination is pressed in the editor. The key names are the same as those used in the [`KeyboardEvent.key` property](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Takes an object which maps key name combinations (e.g. `Meta+Shift+ArrowDown`) to functions, which return `true` when the key press event is handled, or `false` otherwise. The functions have a single argument:
+
+- `ctx:` An object containing the BlockNote editor instance.
+
+`inputRules?:` Input rules update blocks when given regular expressions are found in them. Takes an array of objects. Each object has a `find` field for the regular expression to find, and a `replace` field, for a function that should run on a match. The function should return a [`PartialBlock`](docs/reference/editor/manipulating-content#partial-blocks) which specifies how the block should be updated, or avoid updating it. It also has a single argument:
+
+- `props:` An object containing the result of the regular expression match, a range for the [Prosemirror position indices](https://prosemirror.net/docs/guide/#doc.indexing) spanned by the match, and the BlockNote editor instance.
+
+`plugins?:` An array of [ProseMirror plugins](https://prosemirror.net/docs/ref/#state.Plugin_System).
+
+`tiptapExtensions?:` An array of [TipTap extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new).
+
+## Adding extensions to the editor
+
+Extensions can be added to the editor on their own via the [editor options](/docs/reference/editor/overview#options) or as part of [custom blocks](/docs/features/custom-schemas/custom-blocks).
+
+### Adding directly to the editor
+
+The `extensions` [editor option](/docs/reference/editor/overview#options) takes an array of extensions to be added to the editor:
+
+```typescript
+const editor = useCreateBlockNote({
+ extensions: [
+ // Add extensions here:
+ createExtension({ ... })
+ ],
+});
+```
+
+### Adding to custom blocks
+
+When creating a [custom block](/docs/features/custom-schemas/custom-blocks#creating-a-custom-block-type) using `createReactBlockSpec`, you can pass an array of extensions to the third parameter:
+
+```typescript
+const createCustomBlock = createReactBlockSpec(
+ {
+ // Block config
+ ...
+ },
+ {
+ // Block implementation
+ ...
+ }
+ [
+ // Add extensions here:
+ createExtension({ ... })
+ ],
+});
+```
diff --git a/docs/content/docs/features/import/html.mdx b/docs/content/docs/features/import/html.mdx
new file mode 100644
index 0000000000..6fd6baf347
--- /dev/null
+++ b/docs/content/docs/features/import/html.mdx
@@ -0,0 +1,35 @@
+---
+title: HTML
+description: It's possible to export or import Blocks to and from HTML.
+imageTitle: HTML Ixport
+---
+
+# HTML Import
+
+It's possible to import HTML content into BlockNote blocks, completely client-side.
+
+
+ The functions to import from HTML are considered "lossy"; some information might be dropped when converting HTML to Blocks.
+
+ To serialize Blocks to a non-lossy format (for example, to store the contents of the editor in your backend), simply export the built-in Block format using `JSON.stringify(editor.document)`.
+
+
+
+### HTML to Blocks
+
+Use `tryParseHTMLToBlocks` to parse an HTML string to `Block` objects:
+
+```typescript
+tryParseHTMLToBlocks(html: string): Block[];
+
+// Usage
+const blocksFromHTML = editor.tryParseHTMLToBlocks(html);
+```
+
+`returns:` The blocks parsed from the HTML string.
+
+Tries to create `Block` objects out of any HTML block-level elements, and `InlineContent` objects from any HTML inline elements, though not all HTML tags are recognized. If BlockNote doesn't recognize an element's tag, it will parse it as a paragraph or plain text.
+
+**Demo**
+
+
diff --git a/docs/content/docs/features/import/index.mdx b/docs/content/docs/features/import/index.mdx
new file mode 100644
index 0000000000..53e5a3f5f6
--- /dev/null
+++ b/docs/content/docs/features/import/index.mdx
@@ -0,0 +1,56 @@
+---
+title: Import
+description: It's possible to import content into BlockNote.
+imageTitle: Importing Content
+---
+
+# Importing Content
+
+There are two main paths to importing content into BlockNote:
+
+- **HTML**: (Recommended) Import HTML content into BlockNote.
+- **Markdown**: Import Markdown content into BlockNote.
+
+## Migrating Between Editors
+
+When switching editors, there are several migration strategies to consider:
+
+- **Legacy Editor Approach**: Keep both the old and new editors running in parallel. Use the legacy editor for existing content while creating new content in BlockNote.
+ - Minimizes disruption to your existing application
+ - Can segment usage by content type, organization, or other criteria
+- **Hard Cutoff**: Migrate all content at once on a specific date
+ - Provides a clean break and fresh start
+ - May require more upfront preparation
+- **Gradual Migration**: Convert content progressively, such as when files are opened
+ - Smoother transition with less immediate impact
+ - Migration period may extend over a longer time
+
+Choose the strategy that best fits your specific needs and constraints.
+
+### Importing to BlockNote
+
+The recommended approach for importing content into BlockNote is to convert your source content to HTML first, then use `editor.tryParseHTMLToBlocks`:
+
+```tsx
+const existingContent = "
This is a paragraph.
";
+
+const blocks = await editor.tryParseHTMLToBlocks(existingContent);
+
+await storeToDB(blocks);
+```
+
+
+ For details on server-side processing, see our [server-side
+ guide](/docs/features/server-processing).
+
+
+### Migrating from BlockNote
+
+To migrate content out of BlockNote, convert it to HTML using the `editor.blocksToHTMLLossy` method.
+
+HTML is widely supported and can be easily imported into most other editors.
+
+
+ For details on server-side processing, see our [server-side
+ guide](/docs/features/server-processing).
+
diff --git a/docs/content/docs/features/import/markdown.mdx b/docs/content/docs/features/import/markdown.mdx
new file mode 100644
index 0000000000..3106fd8541
--- /dev/null
+++ b/docs/content/docs/features/import/markdown.mdx
@@ -0,0 +1,41 @@
+---
+title: Markdown
+description: It's possible to import Markdown content into BlockNote blocks, completely client-side.
+imageTitle: Markdown Import
+---
+
+# Markdown Import
+
+
+ The functions to import from Markdown are considered "lossy"; some information might be dropped when converting Markdown to Blocks.
+
+ To serialize Blocks to a non-lossy format (for example, to store the contents of the editor in your backend), simply export the built-in Block format using `JSON.stringify(editor.document)`.
+
+
+
+BlockNote can import Markdown content into Block objects. Note that this is considered "lossy", as not all Markdown structures can be entirely represented as BlockNote blocks.
+
+
+ **BlockNote ships a minimal Markdown parser.** It covers the common subset used by most users (CommonMark + GFM basics: headings, paragraphs, lists, task lists, tables, code, blockquotes, links, images, emphasis, strikethrough, hard breaks).
+
+ There are many Markdown specifications (CommonMark, GFM, MDX, Pandoc, and various dialect-specific extensions) and supporting all of them inside a rich text editor is not a goal of BlockNote. **If you need to handle Markdown beyond this minimal subset, parse it to HTML yourself with a parser of your choice (e.g. [`marked`](https://github.com/markedjs/marked), [`markdown-it`](https://github.com/markdown-it/markdown-it), or [`remark`](https://github.com/remarkjs/remark)) and pass the resulting HTML to [`tryParseHTMLToBlocks`](/docs/features/import) instead.** BlockNote's HTML interoperability is much broader, since HTML is the format the editor uses internally for arbitrary pastes.
+
+
+## Markdown to Blocks
+
+Use `tryParseMarkdownToBlocks` to try parsing a Markdown string into `Block` objects:
+
+```typescript
+tryParseMarkdownToBlocks(markdown: string): Block[];
+
+// Usage
+const blocksFromMarkdown = editor.tryParseMarkdownToBlocks(markdown);
+```
+
+`returns:` The blocks parsed from the Markdown string.
+
+Tries to create `Block` and `InlineContent` objects based on Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it as text.
+
+**Demo**
+
+
diff --git a/docs/content/docs/features/localization.mdx b/docs/content/docs/features/localization.mdx
new file mode 100644
index 0000000000..5791f052c4
--- /dev/null
+++ b/docs/content/docs/features/localization.mdx
@@ -0,0 +1,219 @@
+---
+title: Localization (i18n)
+description: Learn how to localize BlockNote to support multiple languages and customize text strings
+---
+
+# Localization (i18n)
+
+BlockNote is designed to be fully localized, with support for multiple languages. You can easily change the language of your editor or create custom translations.
+
+## Supported Languages
+
+BlockNote supports the following languages out of the box:
+
+- **Arabic** (`ar`) - العربية
+- **Chinese (Simplified)** (`zh`) - 中文
+- **Chinese (Traditional)** (`zh-tw`) - 繁體中文
+- **Croatian** (`hr`) - Hrvatski
+- **Dutch** (`nl`) - Nederlands
+- **English** (`en`) - English
+- **French** (`fr`) - Français
+- **German** (`de`) - Deutsch
+- **Hebrew** (`he`) - עברית
+- **Icelandic** (`is`) - Íslenska
+- **Italian** (`it`) - Italiano
+- **Japanese** (`ja`) - 日本語
+- **Korean** (`ko`) - 한국어
+- **Norwegian** (`no`) - Norsk
+- **Polish** (`pl`) - Polski
+- **Portuguese** (`pt`) - Português
+- **Russian** (`ru`) - Русский
+- **Slovak** (`sk`) - Slovenčina
+- **Spanish** (`es`) - Español
+- **Ukrainian** (`uk`) - Українська
+- **Uzbek** (`uz`) - O'zbekcha
+- **Vietnamese** (`vi`) - Tiếng Việt
+
+## Basic Usage
+
+To use a different language, import the desired locale and pass it to the `dictionary` option:
+
+```tsx
+import { useCreateBlockNote, BlockNoteView } from "@blocknote/react";
+import { fr } from "@blocknote/core/locales";
+
+function FrenchEditor() {
+ const editor = useCreateBlockNote({
+ dictionary: fr,
+ });
+
+ return ;
+}
+```
+
+## Dynamic Language Switching
+
+You can dynamically change the language based on user preferences or your app's locale:
+
+```tsx
+import { useCreateBlockNote, BlockNoteView } from "@blocknote/react";
+import * as locales from "@blocknote/core/locales";
+
+function LocalizedEditor({ language = "en" }) {
+ const editor = useCreateBlockNote({
+ dictionary: locales[language as keyof typeof locales] || locales.en,
+ });
+
+ return ;
+}
+
+// Usage
+
+
+
+```
+
+## Customizing Text Strings
+
+You can customize specific text strings by extending an existing dictionary:
+
+```tsx
+import { useCreateBlockNote, BlockNoteView } from "@blocknote/react";
+import { en } from "@blocknote/core/locales";
+
+function CustomEditor() {
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ placeholders: {
+ ...en.placeholders,
+ default: "Start typing your story...",
+ heading: "Enter your title here",
+ emptyDocument: "Begin your document",
+ },
+ slash_menu: {
+ ...en.slash_menu,
+ paragraph: {
+ ...en.slash_menu.paragraph,
+ title: "Text Block",
+ subtext: "Regular text content",
+ },
+ },
+ },
+ });
+
+ return ;
+}
+```
+
+## Dictionary Structure
+
+The dictionary object contains translations for various parts of the editor:
+
+### Placeholders
+
+Text that appears when blocks are empty:
+
+```tsx
+placeholders: {
+ default: "Enter text or type '/' for commands",
+ heading: "Heading",
+ bulletListItem: "List",
+ numberedListItem: "List",
+ checkListItem: "List",
+ new_comment: "Write a comment...",
+ edit_comment: "Edit comment...",
+ comment_reply: "Add comment...",
+}
+```
+
+### Slash Menu
+
+Commands that appear when typing `/`:
+
+```tsx
+slash_menu: {
+ paragraph: {
+ title: "Paragraph",
+ subtext: "The body of your document",
+ aliases: ["p", "paragraph"],
+ group: "Basic blocks",
+ },
+ heading: {
+ title: "Heading 1",
+ subtext: "Top-level heading",
+ aliases: ["h", "heading1", "h1"],
+ group: "Headings",
+ },
+ // ... more menu items
+}
+```
+
+### UI Elements
+
+Text for buttons, menus, and other interface elements:
+
+```tsx
+side_menu: {
+ add_block_label: "Add block",
+ drag_handle_label: "Open block menu",
+},
+table_handle: {
+ delete_column_menuitem: "Delete column",
+ add_left_menuitem: "Add column left",
+ // ... more table options
+},
+color_picker: {
+ text_title: "Text",
+ background_title: "Background",
+ colors: {
+ default: "Default",
+ gray: "Gray",
+ // ... more colors
+ },
+}
+```
+
+## Integration with i18n Libraries
+
+You can integrate BlockNote with popular i18n libraries like `react-i18next` or `next-intl`:
+
+```tsx
+import { useCreateBlockNote, BlockNoteView } from "@blocknote/react";
+import { useTranslation } from "react-i18next";
+import * as locales from "@blocknote/core/locales";
+
+function I18nEditor() {
+ const { i18n } = useTranslation();
+
+ const editor = useCreateBlockNote({
+ dictionary: locales[i18n.language as keyof typeof locales] || locales.en,
+ });
+
+ return ;
+}
+```
+
+## Adding New Languages
+
+To add support for a new language, you can:
+
+1. **Submit a Pull Request** to the BlockNote repository with your translations
+2. **Create a custom dictionary** in your application for immediate use
+
+When creating translations, make sure to:
+
+- Translate all text strings in the dictionary
+- Maintain the same structure as the English dictionary
+- Test the translations with different content types
+- Consider cultural differences in UI text
+
+## Examples
+
+### Basic Localization
+
+
+
+### Custom Placeholders
+
+
diff --git a/docs/content/docs/features/meta.json b/docs/content/docs/features/meta.json
new file mode 100644
index 0000000000..97ed9ffce7
--- /dev/null
+++ b/docs/content/docs/features/meta.json
@@ -0,0 +1,14 @@
+{
+ "title": "Features",
+ "pages": [
+ "ai",
+ "blocks",
+ "collaboration",
+ "export",
+ "import",
+ "server-processing",
+ "localization",
+ "extensions",
+ "..."
+ ]
+}
diff --git a/docs/content/docs/features/server-processing.mdx b/docs/content/docs/features/server-processing.mdx
new file mode 100644
index 0000000000..8488e00639
--- /dev/null
+++ b/docs/content/docs/features/server-processing.mdx
@@ -0,0 +1,72 @@
+---
+title: Server-side processing
+description: Use `ServerBlockNoteEditor` to process Blocks on the server.
+path: /docs/server-side-processing
+---
+
+# Server-side Processing
+
+While you can use the `BlockNoteEditor` on the client side, you can also use `ServerBlockNoteEditor` from `@blocknote/server-util` to process BlockNote documents on the server.
+
+For example, use the following code to convert a BlockNote document to HTML on the server:
+
+```tsx
+import { ServerBlockNoteEditor } from "@blocknote/server-util";
+
+const editor = ServerBlockNoteEditor.create();
+const html = await editor.blocksToFullHTML(blocks);
+```
+
+`ServerBlockNoteEditor.create` takes the same BlockNoteEditorOptions as `useCreateBlockNote` and `BlockNoteEditor.create` ([see docs](/docs/getting-started)),
+so you can pass the same configuration (for example, your custom schema) to your server-side BlockNote editor as on the client.
+
+## Functions for converting blocks
+
+`ServerBlockNoteEditor` exposes the same functions for converting blocks as the client side editor ([HTML](/docs/features/import/html), [Markdown](/docs/features/import/markdown)):
+
+- `blocksToFullHTML`
+- `blocksToHTMLLossy` and `tryParseHTMLToBlocks`
+- `blocksToMarkdownLossy` and `tryParseMarkdownToBlocks`
+
+## Yjs processing
+
+Additionally, `ServerBlockNoteEditor` provides functions for processing Yjs documents in case you use Yjs collaboration:
+
+- `yDocToBlocks` or `yXmlFragmentToBlocks`: use this to convert a Yjs document or XML Fragment to BlockNote blocks
+- `blocksToYDoc` or `blocksToYXmlFragment`: use this to convert a BlockNote document (blocks) to a Yjs document or XML Fragment
+
+## React compatibility
+
+If you use [custom schemas in React](/docs/features/custom-schemas), you can use the same schema on the server side.
+Functions like `blocksToFullHTML` will use your custom React rendering functions to export blocks to HTML, similar to how these functions work on the client.
+However, it could be that your React components require access to a React context (e.g. a theme or localization context).
+
+For these use-cases, we provide a function `withReactContext` that allows you to pass a React context to the server-side editor.
+This example exports a BlockNote document to HTML within a React context `YourContext`, so that even Custom Blocks built in React that require `YourContext` will be exported correctly:
+
+```tsx
+const html = await editor.withReactContext(
+ ({ children }) => (
+ {children}
+ ),
+ async () => editor.blocksToFullHTML(blocks),
+);
+```
+
+## Next.js App Router
+
+If you're using `@blocknote/server-util` in a Next.js App Router API route (Route Handler), you need to add the BlockNote packages to `serverExternalPackages` in your `next.config.ts`:
+
+```typescript
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ serverExternalPackages: [
+ "@blocknote/core",
+ "@blocknote/react",
+ "@blocknote/server-util",
+ ],
+};
+
+export default nextConfig;
+```
diff --git a/docs/content/docs/foundations/document-structure.mdx b/docs/content/docs/foundations/document-structure.mdx
new file mode 100644
index 0000000000..736671dae3
--- /dev/null
+++ b/docs/content/docs/foundations/document-structure.mdx
@@ -0,0 +1,138 @@
+---
+title: Document Structure
+description: Learn how documents (the content of the rich text editor) are structured to make the most out of BlockNote.
+---
+
+# Document Structure
+
+Each BlockNote document is made up of a list of blocks.
+A block is a piece of content like a paragraph, heading, list item or image. Blocks can be dragged around by users in the editor. A block contains a piece of content and optionally nested (child) blocks:
+
+
+
+## Blocks
+
+The `Block` type is used to describe any given block in the editor:
+
+```typescript
+type Block = {
+ id: string;
+ type: string;
+ props: Record;
+ content: InlineContent[] | TableContent | undefined;
+ children: Block[];
+};
+```
+
+### Block Properties
+
+- **`id`**: The block's ID. Multiple blocks cannot share a single ID, and a block will keep the same ID from when it's created until it's removed.
+
+- **`type`**: The block's type, such as a paragraph, heading, or list item. For an overview of built-in block types, see [Built-in Blocks](/docs/features/blocks).
+
+- **`props`**: The block's properties, which is a set of key/value pairs that further specify how the block looks and behaves. Different block types have different props - see [Built-in Blocks](/docs/features/blocks) for more.
+
+- **`content`**: The block's rich text content, usually represented as an array of `InlineContent` objects. This does not include content from any nested blocks. Read on to [Inline Content](#inline-content) for more on this.
+
+- **`children`**: Any blocks nested inside the block. The nested blocks are also represented using `Block` objects.
+
+## Inline Content
+
+The `content` field of a block contains the rich-text content of a block. This is defined as an array of `InlineContent` objects. Inline content can either be styled text or a link (or a custom inline content type if you customize the editor schema).
+
+### Inline Content Objects
+
+The `InlineContent` type is used to describe a piece of inline content:
+
+```typescript
+type Link = {
+ type: "link";
+ content: StyledText[];
+ href: string;
+};
+
+type StyledText = {
+ type: "text";
+ text: string;
+ styles: Styles;
+};
+
+type CustomInlineContent = {
+ type: string;
+ content: StyledText[] | undefined;
+ props: Record;
+};
+
+type InlineContent = Link | StyledText | CustomInlineContent;
+```
+
+The `styles` property is explained below.
+
+### Styles and Rich Text
+
+The `styles` property of `StyledText` objects is used to describe the rich text styles (e.g.: bold, italic, color) or other attributes of a piece of text. It's a set of key / value pairs that specify the styles applied to the text.
+
+See the [Default Styles](/docs/features/blocks/inline-content#default-styles) to learn which styles are included in BlockNote by default.
+
+## See it for yourself
+
+The demo below shows the editor contents (document) in JSON. It's an array of `Block` objects that updates as you type in the editor:
+
+
+
+## Special Cases
+
+While most blocks use an array of `InlineContent` objects to describe their content (e.g.: paragraphs, headings, list items), some blocks, like [images](/docs/features/blocks/embeds#image), don't contain any rich text content, so their `content` fields will be `undefined`.
+
+### Column Blocks
+
+The `@blocknote/xl-multi-column` package allows you to organize blocks side-by-side in columns. It introduces 2 additional block types, the column and column list:
+
+```typescript
+type ColumnBlock = {
+ id: string;
+ type: "column";
+ props: { width: number };
+ content: undefined;
+ children: Block[];
+};
+
+type ColumnListBlock = {
+ id: string;
+ type: "columnList";
+ props: {};
+ content: undefined;
+ children: ColumnBlock[];
+};
+```
+
+While both of these act as regular blocks, there are a few additional restrictions to have in mind when working with them:
+
+- Children of columns must be regular blocks
+- Children of column lists must be columns
+- There must be at least 2 columns in a column list
+
+### Tables
+
+[Tables](/docs/features/blocks/tables) are also different, as they contain `TableContent`. Here, each table cell is represented as an array of `InlineContent` objects:
+
+```typescript
+type TableContent = {
+ type: "tableContent";
+ columnWidths: (number | undefined)[];
+ headerRows?: number;
+ headerCols?: number;
+ rows: {
+ cells: InlineContent[][];
+ }[];
+};
+```
diff --git a/docs/content/docs/foundations/manipulating-content.mdx b/docs/content/docs/foundations/manipulating-content.mdx
new file mode 100644
index 0000000000..5fa4584310
--- /dev/null
+++ b/docs/content/docs/foundations/manipulating-content.mdx
@@ -0,0 +1,142 @@
+---
+title: Manipulating Blocks
+description: Learn how to manipulate blocks in the editor.
+---
+
+# Manipulating Blocks
+
+BlockNote operates on a **block-based architecture**, where all content is organized into discrete blocks. Understanding how to manipulate these blocks is fundamental to working with BlockNote effectively.
+
+## Block-Based Architecture
+
+In BlockNote, everything is a block. A paragraph is a block, a heading is a block, a list item is a block, and even complex structures like tables are composed of blocks. This unified approach makes document manipulation consistent and predictable.
+
+## Core Block Operations
+
+BlockNote provides a comprehensive set of operations for manipulating blocks, all working at the block level:
+
+### Reading Blocks
+
+- **Get the entire document** - Retrieve all top-level blocks
+- **Get specific blocks** - Access individual blocks by ID or reference
+- **Navigate relationships** - Find previous, next, or parent blocks
+- **Traverse all blocks** - Iterate through the entire document structure
+
+[See more in the API reference](/docs/reference/editor/manipulating-content#reading-blocks)
+
+```typescript
+// Get the entire document
+const document: Block[] = editor.document;
+
+// Get a specific block
+const block = editor.getBlock(blockId);
+
+// Get the previous block
+const previousBlock = editor.getPreviousBlock(blockId);
+
+// Get the next block
+const nextBlock = editor.getNextBlock(blockId);
+```
+
+### Creating Blocks
+
+- **Insert new blocks** - Add blocks before or after existing ones
+- **Create complex structures** - Build nested blocks like lists and tables
+- **Generate blocks programmatically** - Create blocks from data or user input
+
+[See more in the API reference](/docs/reference/editor/manipulating-content#inserting-blocks)
+
+```typescript
+// Insert a simple paragraph block
+editor.insertBlocks(
+ [{ type: "paragraph", content: "Hello, world!" }],
+ referenceBlock,
+);
+
+// Create a complex block structure
+editor.insertBlocks(
+ [
+ { type: "heading", content: "My Heading" },
+ { type: "paragraph", content: "Some content" },
+ { type: "bulletListItem", content: "List item 1" },
+ { type: "bulletListItem", content: "List item 2" },
+ ],
+ referenceBlock,
+);
+```
+
+### Modifying Blocks
+
+- **Update existing blocks** - Change block type, content, or properties
+- **Replace blocks** - Swap one or more blocks with new blocks
+- **Move blocks** - Reorder blocks by moving them up or down
+- **Nest and unnest** - Change the hierarchy by indenting or outdenting blocks
+
+[See more in the API reference](/docs/reference/editor/manipulating-content#updating-blocks)
+
+```typescript
+// Change a block's type
+editor.updateBlock(blockId, { type: "heading" });
+
+// Update block content and properties
+editor.updateBlock(blockId, {
+ content: "Updated content",
+ props: { level: 2 },
+});
+```
+
+### Removing Blocks
+
+- **Delete specific blocks** - Remove individual blocks or groups of blocks
+- **Clear selections** - Remove blocks based on user selection
+
+[See more in the API reference](/docs/reference/editor/manipulating-content#removing-blocks)
+
+```typescript
+// Remove specific blocks
+editor.removeBlocks([blockId1, blockId2]);
+
+// Replace blocks with new blocks
+editor.replaceBlocks(
+ [oldBlockId],
+ [{ type: "paragraph", content: "New content" }],
+);
+```
+
+## Working with Cursor and Selections
+
+- **Read cursor position** - Get information about where the user's cursor is located
+- **Set cursor position** - Move the cursor to specific blocks
+- **Read selections** - Access blocks currently selected by the user
+- **Set selections** - Programmatically select ranges of blocks
+
+[See more in the API reference](/docs/reference/editor/cursor-selections)
+
+```typescript
+// Get cursor position information
+const cursorPosition = editor.getTextCursorPosition();
+
+// Set cursor to a specific block
+editor.setTextCursorPosition(blockId, "start");
+
+// Get current selection
+const selection = editor.getSelection();
+
+// Set selection programmatically
+editor.setSelection(startBlockId, endBlockId);
+```
+
+## Best Practices
+
+1. **Work with block references** - Use existing blocks as references for positioning new blocks
+2. **Handle errors gracefully** - Operations can fail if blocks don't exist or are invalid
+3. **Consider user experience** - Think about how your block manipulations affect the user's workflow
+4. **Group related operations** - Use [transactions](/docs/reference/editor/overview#transactions) to group multiple block changes into a single undo/redo operation
+
+## Next Steps
+
+This overview covers the fundamental concepts of block manipulation in BlockNote. For detailed API reference and specific examples, see:
+
+- [Manipulating Blocks Reference](/docs/reference/editor/manipulating-content) - Complete API documentation
+- [Cursor & Selections](/docs/reference/editor/cursor-selections) - Working with user selections
+- [Block Types](/docs/features/blocks) - Understanding different block types and their properties
diff --git a/docs/content/docs/foundations/meta.json b/docs/content/docs/foundations/meta.json
new file mode 100644
index 0000000000..3bef23596c
--- /dev/null
+++ b/docs/content/docs/foundations/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Foundations",
+ "pages": ["document-structure", "schemas", "manipulating-content", "..."]
+}
diff --git a/docs/content/docs/foundations/schemas.mdx b/docs/content/docs/foundations/schemas.mdx
new file mode 100644
index 0000000000..cd4c0f679c
--- /dev/null
+++ b/docs/content/docs/foundations/schemas.mdx
@@ -0,0 +1,12 @@
+---
+title: Schemas
+description: Learn about how content types are defined in the editor.
+---
+
+# Schemas
+
+Schemas are the core of how BlockNote works. They are the basic building blocks of the editor, and are used to define the content of the editor.
+
+The schema is a collection of definitions for the different types of blocks, inline content, and styles that can be used in the editor.
+
+By default, BlockNote contains everything found under the [Built-in Blocks](/docs/features/blocks) section. You can also modify this default schema, or create your own from scratch - see [Custom Schemas](/docs/features/custom-schemas) to learn how.
diff --git a/docs/content/docs/foundations/supported-formats.mdx b/docs/content/docs/foundations/supported-formats.mdx
new file mode 100644
index 0000000000..c5e302e19a
--- /dev/null
+++ b/docs/content/docs/foundations/supported-formats.mdx
@@ -0,0 +1,250 @@
+---
+title: Format Interoperability
+description: Learn about the formats BlockNote supports for importing and exporting content.
+---
+
+# Format Interoperability
+
+BlockNote is compatible with a few different storage formats, each with its own advantages and disadvantages. This guide will show you how to use each of them.
+
+## Overview
+
+When it comes to editors, formats can be tricky. The editor needs to be able to both read and write to each format.
+
+
+ If elements are not preserved in this transformation, we call the conversion
+ _lossy_. While we'd ideally support every format, **other formats may not
+ support all BlockNote content.**
+
+
+See the table below for a summary of the formats we support and their lossiness:
+
+| Format | Import | Export | [Pro Only](/pricing) |
+| :------------------------------------------------------------------------ | :--------- | :--------- | :------------------- |
+| **BlockNote JSON (`editor.document`)** | ✅ | ✅ | ❌ |
+| **BlockNote HTML (`blocksToFullHTML`)** | ✅ | ✅ | ❌ |
+| **Standard HTML (`blocksToHTMLLossy`)** | ✅ (lossy) | ✅ (lossy) | ❌ |
+| **Markdown (`blocksToMarkdownLossy`)** | ✅ (lossy) | ✅ (lossy) | ❌ |
+| **[PDF](/docs/features/export/pdf)** (`@blocknote/xl-pdf-exporter`) | ❌ | ✅ | ✅ |
+| **[DOCX](/docs/features/export/docx)** (`@blocknote/xl-docx-exporter`) | ❌ | ✅ | ✅ |
+| **[ODT](/docs/features/export/odt)** (`@blocknote/xl-odt-exporter`) | ❌ | ✅ | ✅ |
+| **[Email](/docs/features/export/email)** (`@blocknote/xl-email-exporter`) | ❌ | ✅ | ✅ |
+
+
+ **Tip:** It's recommended to use **BlockNote JSON (`editor.document`)** for
+ storing your documents, as it's the most durable format & guaranteed to be
+ lossless.
+
+
+## Working with Blocks (JSON)
+
+BlockNote uses a JSON structure (an array of `Block` objects) as its native format. This is the recommended way to store documents as it's **lossless**, preserving the exact structure and all attributes of your content.
+
+### Saving Blocks
+
+The best way to get the latest content is to use the `editor.onChange` callback if using vanilla JS or `useEditorChange` hook if using React. This function is called every time the editor's content changes.
+
+```tsx twoslash
+import React from "react";
+import { useCreateBlockNote, useEditorChange } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+
+// ---cut-start---
+function storeToDB(blocks: string) {
+ console.log(blocks);
+}
+// ---cut-end---
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ useEditorChange((editor) => {
+ // The current document content as a string
+ const savedBlocks = JSON.stringify(editor.document);
+ // ^^^^^^^^^^^^^^^
+
+ storeToDB(savedBlocks);
+ }, editor);
+
+ return ;
+}
+```
+
+### Loading Blocks
+
+To load content, you can use the `initialContent` prop when creating the editor. You can pass the array of `Block` objects you previously saved.
+
+```tsx
+import { useCreateBlockNote } from "@blocknote/react";
+import type { Block } from "@blocknote/core";
+import { BlockNoteView } from "@blocknote/mantine";
+
+export default function App({
+ initialContent,
+}: {
+ initialContent?: Block[];
+}) {
+ const editor = useCreateBlockNote({
+ initialContent,
+ });
+
+ return ;
+}
+```
+
+## Working with HTML
+
+BlockNote provides utilities to convert content between `Block` objects and HTML. Note that converting to standard HTML can be **lossy**.
+
+### Saving as HTML
+
+To convert the document to an HTML string, you can use `editor.blocksToFullHTML(blocks: Block[])`:
+
+```tsx twoslash
+import React from "react";
+import { useCreateBlockNote, useEditorChange } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+
+// ---cut-start---
+function storeToDB(html: string) {
+ console.log(html);
+}
+// ---cut-end---
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ useEditorChange(async (editor) => {
+ const html = await editor.blocksToFullHTML(editor.document);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ // You can now save this HTML string
+ storeToDB(html);
+ }, editor);
+
+ return ;
+}
+```
+
+
+ The `editor.blocksToFullHTML` method will output HTML in the BlockNote
+ internal format. If you want to export to standard HTML, you can use
+ `editor.blocksToHTMLLossy` instead.
+
+
+### Loading from HTML
+
+To load HTML content, you first need to convert it to an array of `Block` objects using `editor.tryParseHTMLToBlocks()`. Then, you can insert it into the editor.
+
+```tsx twoslash
+import React from "react";
+import { useEffect } from "react";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+
+const myHTML = "
This is a paragraph.
";
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ useEffect(() => {
+ // Replaces the blocks on initialization
+ // But, you can also call this before rendering the editor
+ async function loadHTML() {
+ const blocks = await editor.tryParseHTMLToBlocks(myHTML);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ editor.replaceBlocks(editor.document, blocks);
+ }
+ loadHTML();
+ }, [editor]);
+
+ return ;
+}
+```
+
+## Working with Markdown
+
+BlockNote also supports converting to and from Markdown. However, converting to and from Markdown is a **lossy** conversion.
+
+
+ BlockNote ships a **minimal** Markdown parser/serializer that targets the
+ common CommonMark + GFM subset (headings, paragraphs, lists, task lists,
+ tables, code, blockquotes, links, images, emphasis, strikethrough, hard
+ breaks). Supporting every Markdown dialect (CommonMark, GFM, MDX, Pandoc,
+ and various extensions) is not a goal for the editor. If your use case
+ requires Markdown features beyond this subset, **parse the Markdown to
+ HTML yourself** (with a library like [`marked`](https://github.com/markedjs/marked),
+ [`markdown-it`](https://github.com/markdown-it/markdown-it), or
+ [`remark`](https://github.com/remarkjs/remark)) and feed the resulting
+ HTML to `editor.tryParseHTMLToBlocks` — HTML is the format BlockNote uses
+ for arbitrary pastes and has much broader interoperability.
+
+
+### Saving as Markdown
+
+To convert the document to a Markdown string, you can use `editor.blocksToMarkdownLossy()`:
+
+```tsx twoslash
+import React from "react";
+import { useCreateBlockNote, useEditorChange } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+
+// ---cut-start---
+function storeToDB(markdown: string) {
+ console.log(markdown);
+}
+// ---cut-end---
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ useEditorChange(async (editor) => {
+ const markdown = await editor.blocksToMarkdownLossy(editor.document);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ // You can now save this Markdown string
+ storeToDB(markdown);
+ }, editor);
+
+ return ;
+}
+```
+
+### Loading from Markdown
+
+To load Markdown content, you first need to convert it to an array of `Block` objects using `editor.tryParseMarkdownToBlocks()`. Then, you can insert it into the editor.
+
+```tsx twoslash
+import React from "react";
+import { useEffect } from "react";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+
+const myMarkdown = "This is a paragraph with **bold** text.";
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ useEffect(() => {
+ async function loadMarkdown() {
+ const blocks = await editor.tryParseMarkdownToBlocks(myMarkdown);
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ editor.replaceBlocks(editor.document, blocks);
+ }
+ loadMarkdown();
+ }, [editor]);
+
+ return ;
+}
+```
+
+## Export Only
+
+BlockNote can also export to these additional formats:
+
+- DOCX
+ - Via the [`@blocknote/xl-docx-exporter` package](/docs/features/export/docx)
+- PDF
+ - Via the [`@blocknote/xl-pdf-exporter` package](/docs/features/export/pdf)
+- ODT
+ - Via the [`@blocknote/xl-odt-exporter` package](/docs/features/export/odt)
+- Email
+ - Via the [`@blocknote/xl-email-exporter` package](/docs/features/export/email)
diff --git a/docs/content/docs/getting-started/ariakit.mdx b/docs/content/docs/getting-started/ariakit.mdx
new file mode 100644
index 0000000000..bb5a2b2036
--- /dev/null
+++ b/docs/content/docs/getting-started/ariakit.mdx
@@ -0,0 +1,24 @@
+---
+title: With Ariakit
+description: Ariakit rich text editor with BlockNote
+---
+
+# Getting Started With Ariakit
+
+[Ariakit](https://ariakit.org/) is an open-source library of unstyled (headless), primitive components with a focus on Accessibility.
+
+```console tab="npm"
+npm install @blocknote/core @blocknote/react @blocknote/ariakit
+```
+
+```console tab="pnpm"
+pnpm add @blocknote/core @blocknote/react @blocknote/ariakit
+```
+
+```console tab="bun"
+bun add @blocknote/core @blocknote/react @blocknote/ariakit
+```
+
+To use BlockNote with Ariakit, you can import `BlockNoteView` from `@blocknote/ariakit`. You can fully style the components with your own CSS, or import the provided default styles using the `@blocknote/ariakit/style.css` stylesheet.
+
+
diff --git a/docs/content/docs/getting-started/editor-setup.mdx b/docs/content/docs/getting-started/editor-setup.mdx
new file mode 100644
index 0000000000..3881e88acd
--- /dev/null
+++ b/docs/content/docs/getting-started/editor-setup.mdx
@@ -0,0 +1,86 @@
+---
+title: Editor Setup
+description: Learn how to set up the editor.
+---
+
+# Editor Setup
+
+You can customize your editor when you instantiate it. Let's take a closer looks at the basic methods and components to set up your BlockNote editor.
+
+## Create an editor
+
+Create a new `BlockNoteEditor` by calling the `useCreateBlockNote` hook. This instantiates a new editor and its required state. You can later interact with the editor using the Editor API and pass it to the `BlockNoteView` component.
+
+```tsx twoslash
+import React from "react";
+/**
+ * The options for the editor, like initial content, schema, etc.
+ * See the [Editor Options API reference](/docs/reference/editor/overview#options) for more details
+ */
+type BlockNoteEditorOptions = object;
+/**
+ * See the [Editor API reference](/docs/reference/editor/manipulate-blocks) for more details
+ */
+type BlockNoteEditor = object;
+/**
+ * This hook creates a new editor instance, but doesn't render it.
+ */
+// ---cut---
+declare function useCreateBlockNote(
+ options?: BlockNoteEditorOptions,
+ deps?: React.DependencyList,
+): BlockNoteEditor;
+```
+
+The hook takes two optional parameters:
+
+**options:** Configure the editor with various options. You can find some commonly used options below, or see [Editor Options](/docs/reference/editor/overview#options) for all available options.
+
+- `initialContent` - Set starting content
+- `dictionary` - Customize text strings for localization. See the [Localization](/docs/features/localization) for more.
+- `schema` - Add custom blocks and styles. See [Custom Schemas](/docs/features/custom-schemas).
+- `uploadFile` - Handle file uploads to a backend.
+- `pasteHandler` - Handle how pasted clipboard content gets parsed.
+
+**deps:** React dependency array that determines when to recreate the editor.
+
+
+ Manually creating the editor (`BlockNoteEditor.create`)
+
+ The `useCreateBlockNote` hook is actually a simple `useMemo` wrapper around
+ the `BlockNoteEditor.create` method. You can use this method directly if you
+ want to control the editor lifecycle manually. For example, we do this in
+ the [Saving & Loading example](/examples/backend/saving-loading) to delay
+ the editor creation until some content has been fetched from an external
+ data source.
+
+
+
+## Render the editor
+
+Use the `` component to render the `BlockNoteEditor` instance you just created:
+
+```tsx
+const editor = useCreateBlockNote();
+
+return ;
+```
+
+The `` component has a number of props that you can use to customize the editor. See [React Overview](/docs/react/overview) for more information. But, here are some important props to consider:
+
+- `editor`: The `BlockNoteEditor` instance to render.
+- `editable`: Whether the editor should be editable.
+- `onChange`: Callback fired when the editor content (document) changes.
+- `onSelectionChange`: Callback fired when the editor selection changes.
+- `theme`: The editor's theme, see [Themes](/docs/react/styling-theming/themes) for more about this.
+
+
+ Uncontrolled component
+
+ Note that the `BlockNoteView` component is an [uncontrolled component](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).
+ This means you don't pass in the editor content directly as a prop. You can use the `initialContent` option in the `useCreateBlockNote` hook to set the initial content of the editor (similar to the `defaultValue` prop in a regular React `
+
+ BlockNote handles the complexities and performance optimizations of managing editor state internally. You can interact with the editor content using the [Editor API](/docs/reference/editor/overview).
+
+
diff --git a/docs/content/docs/getting-started/index.mdx b/docs/content/docs/getting-started/index.mdx
new file mode 100644
index 0000000000..0ee82296f4
--- /dev/null
+++ b/docs/content/docs/getting-started/index.mdx
@@ -0,0 +1,76 @@
+---
+title: Getting Started
+description: Getting started with BlockNote is quick and easy. All you need to do is install the package and add the React component to your app!
+imageTitle: Getting Started with BlockNote
+---
+
+# Getting Started
+
+Getting started with BlockNote is quick and easy. Install the required packages and add the React component to your app.
+
+## Install
+
+To install BlockNote with [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), run:
+
+```console tab="npm"
+npm install @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+```console tab="pnpm"
+pnpm add @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+```console tab="bun"
+bun add @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+This guide uses the [Mantine](https://mantine.dev/) version of BlockNote's UI, which works great out-of-the-box. However, BlockNote provides multiple UI library options. Choose the one that best fits your project:
+
+- **[Mantine](/docs/getting-started/mantine)** (`@blocknote/mantine`) - Recommended for new projects
+- **[Shadcn](/docs/getting-started/shadcn)** (`@blocknote/shadcn`)
+- **[Ariakit](/docs/getting-started/ariakit)** (`@blocknote/ariakit`)
+
+## Create an editor
+
+The fastest way to get started with the BlockNote is by using the `useCreateBlockNote` hook and `BlockNoteView` component:
+
+```tsx
+import React from "react";
+import { useCreateBlockNote } from "@blocknote/react";
+// Or, you can use ariakit, shadcn, etc.
+import { BlockNoteView } from "@blocknote/mantine";
+// Default styles for the mantine editor
+import "@blocknote/mantine/style.css";
+// Include the included Inter font
+import "@blocknote/core/fonts/inter.css";
+
+export default function MyEditor() {
+ // Create a new editor instance
+ const editor = useCreateBlockNote();
+
+ // Render the editor
+ return ;
+}
+```
+
+For more information about the `useCreateBlockNote` hook and the `BlockNoteView` component, see [React Overview](/docs/react/overview).
+
+
+ Are you using Next.js (`create-next-app`)? Because BlockNote is a client-only
+ component, make sure to disable server-side rendering of BlockNote. [Read our
+ guide on setting up Next.js + BlockNote](/docs/getting-started/nextjs)
+
+
+## Next steps
+
+You now know how to integrate BlockNote into your React app! However, this is just scratching the surface of what you can do with BlockNote.
+
+- Learn about [blocks and the editor basics](/docs/foundations/document-structure) and how to interact with the editor using the [editor API](/docs/reference/editor/manipulating-content)
+- See [UI Components](/docs/react/components) to customize built-in menus and toolbars and [Styling & Theming](/docs/react/styling-theming) to customize the look and feel of the editor
+- Further extend the editor with your own Blocks using [Custom Schemas](/docs/features/custom-schemas) or add [Real-Time Collaboration](/docs/features/collaboration)
diff --git a/docs/content/docs/getting-started/mantine.mdx b/docs/content/docs/getting-started/mantine.mdx
new file mode 100644
index 0000000000..013b7d3493
--- /dev/null
+++ b/docs/content/docs/getting-started/mantine.mdx
@@ -0,0 +1,34 @@
+---
+title: With Mantine
+description: Mantine rich text editor using BlockNote
+---
+
+# Getting Started With Mantine
+
+[Mantine](https://mantine.dev/) is an open-source collection of React components.
+
+```console tab="npm"
+npm install @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+```console tab="pnpm"
+pnpm add @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+```console tab="bun"
+bun add @blocknote/core @blocknote/react @blocknote/mantine
+npm install @mantine/core @mantine/hooks @mantine/utils
+```
+
+To use BlockNote with Mantine, you can import `BlockNoteView` from `@blocknote/mantine` and the stylesheet from `@blocknote/mantine/style.css`.
+
+
+ If your application already uses Mantine UI, you should use the
+ `@blocknote/mantine/blocknoteStyles.css` stylesheet instead. It only contains
+ the styles added by BlockNote on top of the Mantine core styles, whereas
+ `@blocknote/mantine/style.css` includes both.
+
+
+
diff --git a/docs/content/docs/getting-started/meta.json b/docs/content/docs/getting-started/meta.json
new file mode 100644
index 0000000000..a0232b9082
--- /dev/null
+++ b/docs/content/docs/getting-started/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Getting Started",
+ "pages": ["mantine", "ariakit", "shadcn", "nextjs", "vanilla-js"]
+}
diff --git a/docs/content/docs/getting-started/nextjs.mdx b/docs/content/docs/getting-started/nextjs.mdx
new file mode 100644
index 0000000000..a521ca10a0
--- /dev/null
+++ b/docs/content/docs/getting-started/nextjs.mdx
@@ -0,0 +1,61 @@
+---
+title: With Next.js
+description: Details on integrating BlockNote with Next.js
+---
+
+# Getting Started With Next.js
+
+BlockNote is a component that should only be rendered client-side (and not on the server). If you're using Next.js, you need to make sure that Next.js does not try to render BlockNote as a server-side component.
+
+Make sure to use BlockNote in a [Client Component](https://nextjs.org/docs/getting-started/react-essentials#client-components). You can do this by creating a separate file for your component (**make sure this sits outside of your `pages` or `app` directory, for example `components/Editor.tsx`**), and starting that with `"use client";` [directive](https://react.dev/reference/react/use-client):
+
+```typescript jsx
+"use client"; // this registers as a Client Component
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+// Our component we can reuse later
+export default function Editor() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return ;
+}
+```
+
+## Import as dynamic
+
+In the same directory, create a new file called `DynamicEditor.tsx`:
+Here, we will use [Dynamic Imports](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading) to make sure BlockNote is only imported on the client-side.
+
+You can import the component we just created above using `next/dynamic` in your page:
+
+```typescript jsx
+"use client";
+
+import dynamic from "next/dynamic";
+
+export const Editor = dynamic(() => import("./Editor"), { ssr: false });
+```
+
+## Import in a page / app
+
+Now, you can import the dynamic editor in your page or app:
+
+```typescript jsx
+import { Editor } from "../components/DynamicEditor";
+
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+This should resolve any issues you might run into when embedding BlockNote in your Next.js React app!
diff --git a/docs/content/docs/getting-started/shadcn.mdx b/docs/content/docs/getting-started/shadcn.mdx
new file mode 100644
index 0000000000..da0598ede0
--- /dev/null
+++ b/docs/content/docs/getting-started/shadcn.mdx
@@ -0,0 +1,206 @@
+---
+title: With ShadCN
+description: ShadCN rich text editor using BlockNote
+---
+
+# Getting Started With ShadCN
+
+[shadcn/ui](https://ui.shadcn.com/) is an open-source collection of React components based on [Radix](https://radix-ui.com/) and [TailwindCSS](https://tailwindcss.com/).
+
+```console tab="npm"
+npm install @blocknote/core @blocknote/react @blocknote/shadcn
+```
+
+```console tab="pnpm"
+pnpm add @blocknote/core @blocknote/react @blocknote/shadcn
+```
+
+```console tab="bun"
+bun add @blocknote/core @blocknote/react @blocknote/shadcn
+```
+
+To use BlockNote with ShadCN, you can import `BlockNoteView` from `@blocknote/shadcn` and the stylesheet from `@blocknote/shadcn/style.css`. This version of `BlockNoteView` is expected to be used in apps that are already using ShadCN/TailwindCSS, so it does not import any of those styles itself.
+
+To ensure Tailwind generates the necessary CSS for all utility classes used by BlockNote components, make sure to add the `@source` directive to your stylesheet that imports Tailwind:
+
+```css
+@import "tailwindcss";
+...
+/* Path to your installed `@blocknote/shadcn` package. */
+@source "../node_modules/@blocknote/shadcn";
+```
+
+
+
+## Usage with Tailwind Only
+
+If your app doesn't use ShadCN components and only uses TailwindCSS, you just need to extend your Tailwind theme with ShadCN utility classes to get everything working. You can do this by simply copying the styles below into your stylesheet that imports Tailwind.
+
+```css
+@import "tailwindcss";
+...
+/* Path to your installed `@blocknote/shadcn` package. */
+@source "../node_modules/@blocknote/shadcn";
+...
+@custom-variant dark (&:is(.dark *));
+
+/* Light theme ShadCN CSS variables. */
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+/* Dark theme ShadCN CSS variables. */
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+/* Extending Tailwind theme with ShadCN utility classes. */
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+/* Applies some additional necessary border styles within the editor. */
+@layer base {
+ .bn-shadcn * {
+ @apply border-border outline-ring/50;
+ }
+}
+```
+
+The values used are for the Neutral ShadCN color theme (used in demo above), but you can customize them however you'd like.
+
+This website uses the exact same setup as the one described above, which you can see in [this file](https://github.com/TypeCellOS/BlockNote/blob/main/docs/app/global.css).
+
+## ShadCN Customization
+
+BlockNote comes with default shadcn components. However, it's likely that you have copied and possibly customized your own shadcn components in your project.
+To make BlockNote use the ShadCN components from your project instead of the default ones, you can pass them using the `shadCNComponents` prop of `BlockNoteView`:
+
+```tsx
+import * as Button from "@/components/ui/button"
+import * as Select from "@/components/ui/select"
+
+return (
+
+);
+```
+
+You can pass components from the following ShadCN modules:
+
+- Badge
+- Button
+- Card
+- DropdownMenu
+- Form
+- Input
+- Label
+- Popover
+- Select
+- Tabs
+- Toggle
+- Tooltip
+
+
+ To ensure compatibility, your ShadCN components should not use Portals
+ (comment these out from your DropdownMenu, Popover and Select components).
+
diff --git a/docs/content/docs/getting-started/vanilla-js.mdx b/docs/content/docs/getting-started/vanilla-js.mdx
new file mode 100644
index 0000000000..54aff2fae5
--- /dev/null
+++ b/docs/content/docs/getting-started/vanilla-js.mdx
@@ -0,0 +1,139 @@
+---
+title: With Vanilla JS
+description: BlockNote is mainly designed as a quick and easy drop-in block-based editor for React apps, but can also be used in vanilla JavaScript apps.
+---
+
+# Getting Started With Vanilla JS
+
+BlockNote is mainly designed as a quick and easy drop-in block-based editor for React apps, but can also be used in vanilla JavaScript apps. However, this does involve writing your own UI elements.
+
+
+ We recommend using BlockNote with React so you can use the built-in UI
+ components. This document will explain how you can use BlockNote without
+ React, and write your own components, but this is not recommended as you'll
+ lose the great out-of-the-box experience that BlockNote offers.
+
+
+## Installing with NPM
+
+To install only the vanilla JS parts of BlockNote with [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), run:
+
+```console
+npm install @blocknote/core
+```
+
+## Creating an editor
+
+This is how to create a new BlockNote editor:
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+
+const editor = BlockNoteEditor.create();
+
+editor.mount(document.getElementById("root")); // element to append the editor to
+```
+
+Now, you'll have a plain BlockNote instance on your page. However, it's missing some menus and other UI elements.
+
+## Creating your own UI elements
+
+Because you can't use the built-in React [UI Components](/docs/react/components), you'll need to create and register your own UI elements.
+
+While it's up to you to decide how you want the elements to be rendered, BlockNote provides methods for updating the visibility, position, and state of your elements:
+
+```typescript
+type UIElement =
+ | "formattingToolbar"
+ | "linkToolbar"
+ | "filePanel"
+ | "sideMenu"
+ | "suggestionMenu"
+ | "tableHandles"
+
+const uiElement: UIElement = ...;
+
+editor[uiElement].onUpdate((uiElementState: ...) => {
+ ...;
+})
+```
+
+Let's look at how you could add the [Side Menu]() to your editor:
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+
+const editor = BlockNoteEditor.create();
+editor.mount(document.getElementById("root"));
+
+export function createButton(text: string, onClick?: () => void) {
+ const element = document.createElement("a");
+ element.href = "#";
+ element.text = text;
+ element.style.margin = "10px";
+
+ if (onClick) {
+ element.addEventListener("click", (e) => {
+ onClick();
+ e.preventDefault();
+ });
+ }
+
+ return element;
+}
+
+let element: HTMLElement;
+
+editor.sideMenu.onUpdate((sideMenuState) => {
+ if (!element) {
+ element = document.createElement("div");
+ element.style.background = "gray";
+ element.style.position = "absolute";
+ element.style.padding = "10px";
+ element.style.opacity = "0.8";
+ const addBtn = createButton("+", () => {
+ const blockContent = sideMenuState.block.content;
+ const isBlockEmpty =
+ blockContent !== undefined &&
+ Array.isArray(blockContent) &&
+ blockContent.length === 0;
+
+ if (isBlockEmpty) {
+ editor.setTextCursorPosition(sideMenuState.block);
+ editor.openSuggestionMenu("/");
+ } else {
+ const insertedBlock = editor.insertBlocks(
+ [{ type: "paragraph" }],
+ sideMenuState.block,
+ "after",
+ )[0];
+ editor.setTextCursorPosition(insertedBlock);
+ editor.openSuggestionMenu("/");
+ }
+ });
+ element.appendChild(addBtn);
+
+ const dragBtn = createButton("::", () => {});
+
+ dragBtn.addEventListener("dragstart", (evt) =>
+ editor.sideMenu.blockDragStart(evt, sideMenuState.block),
+ );
+ dragBtn.addEventListener("dragend", editor.sideMenu.blockDragEnd);
+ dragBtn.draggable = true;
+ element.style.display = "none";
+ element.appendChild(dragBtn);
+
+ document.getElementById("root")!.appendChild(element);
+ }
+
+ if (sideMenuState.show) {
+ element.style.display = "block";
+
+ element.style.top = sideMenuState.referencePos.top + "px";
+ element.style.left =
+ sideMenuState.referencePos.x - element.offsetWidth + "px";
+ } else {
+ element.style.display = "none";
+ }
+});
+```
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
new file mode 100644
index 0000000000..6585e7a5e4
--- /dev/null
+++ b/docs/content/docs/index.mdx
@@ -0,0 +1,56 @@
+---
+title: Introduction
+description: BlockNote is a block-based rich-text editor for React, focused on providing a great out-of-the-box experience with minimal setup.
+---
+
+# Introduction to BlockNote
+
+
+
+BlockNote is a block-based rich-text editor for [React](https://reactjs.org/), focused on providing a great out-of-the-box experience with minimal setup.
+
+With BlockNote, we want to make it easy for developers to add a next-generation text editing experience to their app, with a UX that's on-par with industry leaders like Notion, Google Docs or Coda.
+
+Unlike other rich-text editor libraries, BlockNote organizes documents into blocks. This makes it easy for the user to organize their document, and for developers to interact with the document from code.
+
+BlockNote has been created with extensibility in mind. You can customize the document, create custom block types and customize UX elements like menu items. Advanced users can even create their own UI from scratch and use BlockNote with vanilla JavaScript instead of React.
+
+- Jump right into the [quickstart](/docs/getting-started) to get started
+- Learn about [blocks and the editor basics](/docs/foundations/document-structure) and how to interact with the editor using the [editor API](/docs/reference/editor/manipulating-content)
+- See [UI Components](/docs/react/components) to customize built-in menus and toolbars and [Styling & Theming](/docs/react/styling-theming) to customize the look and feel of the editor
+- Further extend the editor with your own Blocks using [Custom Schemas](/docs/features/custom-schemas) or add [Real-Time Collaboration](/docs/features/collaboration)
+
+## Why BlockNote?
+
+There are plenty of libraries out there for creating rich-text editors. In fact, BlockNote is built on top of the widely used [ProseMirror](https://prosemirror.net/) and [TipTap](https://tiptap.dev/).
+
+As powerful as they are, these libraries often have quite a steep learning-curve and require you to customize every single detail of your editor. This can require months of specialized work.
+
+BlockNote instead, offers a great experience with minimal setup, including a ready-made and animated UI.
+
+On top of that, it comes with a modern block-based design. This gives documents more structure, allow for a richer user experience while simultaneously making it easier to customize the editor's functionality.
+
+## Community
+
+We'd love your feedback! If you have questions, need help, or want to contribute reach out to the community on [Discord](https://discord.gg/Qc2QTTH5dF) and [GitHub](https://github.com/TypeCellOS/BlockNote).
+
+## Next: Set up BlockNote
+
+See how to set up your own editor in the [Quickstart](/docs/getting-started). Here's a quick sneak peek in case you can't wait!
+
+
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
new file mode 100644
index 0000000000..ddb4b60134
--- /dev/null
+++ b/docs/content/docs/meta.json
@@ -0,0 +1,21 @@
+{
+ "title": "BlockNote Docs",
+ "description": "BlockNote documentation",
+ "icon": "Building2",
+ "root": true,
+ "pages": [
+ "---Getting Started---",
+ "index",
+ "introduction",
+ "getting-started",
+ "getting-started/editor-setup",
+ "---Foundations---",
+ "...foundations",
+ "---Features---",
+ "...features",
+ "---React---",
+ "...react",
+ "---Editor Reference---",
+ "...reference/editor"
+ ]
+}
diff --git a/docs/content/docs/react/components/formatting-toolbar.mdx b/docs/content/docs/react/components/formatting-toolbar.mdx
new file mode 100644
index 0000000000..962035ba57
--- /dev/null
+++ b/docs/content/docs/react/components/formatting-toolbar.mdx
@@ -0,0 +1,40 @@
+---
+title: Formatting Toolbar
+description: The Formatting Toolbar appears whenever you highlight text in the editor.
+---
+
+# Formatting Toolbar
+
+The Formatting Toolbar appears whenever you highlight text in the editor.
+
+
+
+## Changing the Formatting Toolbar
+
+You can change or replace the Formatting Toolbar with your own React component. In the demo below, 2 buttons are added to the default Formatting Toolbar - one to add a blue text/background, and one to toggle code styles.
+
+
+
+We first define our custom `BlueButton`. The `useComponentsContext` hook gets all components used internally by BlockNote, so we want to use `Components.FormattingToolbar.Button` for this.
+
+We use the `FormattingToolbar` component to create a custom Formatting Toolbar. By specifying its children, we can replace the default buttons in the toolbar with our own.
+
+This custom Formatting Toolbar is passed to a `FormattingToolbarController`, which controls its position and visibility (above or below the highlighted text).
+
+Setting `formattingToolbar={false}` on `BlockNoteView` tells BlockNote not to show the default Formatting Toolbar.
+
+## Changing Block Type Select (Dropdown) Items
+
+The first element in the default Formatting Toolbar is the Block Type Select, and you can change the items in it. The demo makes the Block Type Select work for image blocks by adding an item to it.
+
+
+
+Here, we use the `FormattingToolbar` component but keep the default buttons (we don't pass any children). Instead, we pass our customized Block Type Select items using the `blockTypeSelectItems` prop.
diff --git a/docs/content/docs/react/components/grid-suggestion-menus.mdx b/docs/content/docs/react/components/grid-suggestion-menus.mdx
new file mode 100644
index 0000000000..8fe53197f8
--- /dev/null
+++ b/docs/content/docs/react/components/grid-suggestion-menus.mdx
@@ -0,0 +1,52 @@
+---
+title: Grid Suggestion Menus
+description: In addition to displaying Suggestion Menus as stacks, BlockNote also supports displaying them as grids.
+---
+
+# Grid Suggestion Menus
+
+Grid Suggestion Menus appear when the user enters a trigger character, and text after the character is used to filter the menu items.
+
+Grid Suggestion Menus are similar to regular Suggestion Menus, but results are organized in a grid, and users can use all arrow keys (including left, right) on their keyboard to navigate the results.
+
+## Emoji Picker
+
+The Emoji Picker is a Grid Suggestion Menu that opens with the `:` character (or when selecting emoji item in the Slash Menu).
+
+It only displays once the user types 2 non-whitespace characters a query, to minimize cases where the user only wants to enter the `:` character.
+
+
+
+### Changing Emoji Picker Columns
+
+By default, the Emoji Picker is rendered with 10 columns, but you can change this to any amount. In the demo below, the Emoji Picker is changed to only display 5 columns.
+
+
+
+Passing `emojiPicker={false}` to `BlockNoteView` tells BlockNote not to show the default Emoji Picker. Adding the `GridSuggestionMenuController` with `triggerCharacter={":"}` and `columns={5}` tells BlockNote to show one with 5 columns instead.
+
+### Replacing the Emoji Picker Component
+
+You can replace the React component used for the Emoji Picker with your own, as you can see in the demo below.
+
+
+
+Again, we add a `GridSuggestionMenuController` component with `triggerCharacter={":"}` and set `emojiPicker={false}` to replace the default Emoji Picker.
+
+Now, we also pass a component to its `gridSuggestionMenuComponent` prop. The `gridSuggestionMenuComponent` we pass is responsible for rendering the filtered items. The `GridSuggestionMenuController` controls its position and visibility (below the trigger character), and it also determines which items should be shown. Since we don't specify which items to show (the `getItems` prop isn't defined), it will use the default items for a grid, which are the emojis.
+
+## Creating additional Grid Suggestion Menus
+
+You can add additional Grid Suggestion Menus to the editor, which can use any trigger character. The demo below adds an example Grid Suggestion Menu for mentions, where each item is the first character of the user's name, and opens with the `@` character.
+
+
+
+Changing the column count in the new Grid Suggestion Menu, or the component used to render it, is done the same way as for the [Emoji Picker](/docs/react/components/suggestion-menus). For more information about how the mentions elements work, see [Custom Inline Content](/docs/features/custom-schemas/custom-inline-content).
diff --git a/docs/content/docs/react/components/hyperlink-toolbar.mdx b/docs/content/docs/react/components/hyperlink-toolbar.mdx
new file mode 100644
index 0000000000..436fc88116
--- /dev/null
+++ b/docs/content/docs/react/components/hyperlink-toolbar.mdx
@@ -0,0 +1,32 @@
+---
+title: Link Toolbar
+description: The Link Toolbar appears whenever you hover a link in the editor.
+---
+
+# Link Toolbar
+
+The Link Toolbar appears whenever you hover a link in the editor.
+
+
+
+## Changing the Link Toolbar
+
+You can change or replace the Link Toolbar with your own React component. In the demo below, a button is added to the default Link Toolbar, which opens a browser alert.
+
+
+
+We first define our custom `AlertButton`. The `useComponentsContext` hook gets all components used internally by BlockNote, so we want to use `Components.LinkToolbar.Button` for this.
+
+We use the `LinkToolbar` component to create a custom Link Toolbar. By specifying its children, we can replace the default buttons in the toolbar with our own.
+
+This custom Link Toolbar is passed to a `LinkToolbarController`, which controls its position and visibility (above or below the hovered link).
+
+Setting `linkToolbar={false}` on `BlockNoteView` tells BlockNote not to show the default Link Toolbar.
diff --git a/docs/content/docs/react/components/image-toolbar.mdx b/docs/content/docs/react/components/image-toolbar.mdx
new file mode 100644
index 0000000000..e76c47a1a7
--- /dev/null
+++ b/docs/content/docs/react/components/image-toolbar.mdx
@@ -0,0 +1,44 @@
+---
+title: File Panel
+description: The File Panel appears whenever you select an image that doesn't have a URL, or when you click the "Replace File" button in the Formatting Panel when an image is selected.
+---
+
+# File Panel
+
+The File Panel appears whenever you select a file (e.g. an image or video) that doesn't have a URL, or when you click the "Replace File" button in the [Formatting Toolbar](/docs/react/components/formatting-toolbar) when a file is selected.
+
+
+
+## File Upload
+
+You may notice that upon creating a new BlockNote editor, the "Upload" tab in the File Panel is missing. This is because you must provide BlockNote with a function to handle file uploads using the `uploadFile` [Editor Option](/docs/reference/editor/overview#options):
+
+```ts
+type uploadFile = (file: File) => Promise;
+```
+
+`file:` The file to upload, in this case an image.
+
+`returns:` A `Promise`, which resolves to the URL that the image can be accessed at.
+
+You can use the provided `uploadToTempFilesOrg` function to as a starting point, which uploads files to [tmpfiles.org](https://tmpfiles.org/). However, it's not recommended to use this in a production environment - you should use your own backend:
+
+
+
+## Resolving URLs
+
+Depending on your backend implementation, the URL returned after uploading a file may not point to the file itself, but an API endpoint which lets you access the file. In this case, said file will need to be fetched from when rendering the block.
+
+BlockNote supports this use case using the `resolveFileUrl` [editor option](/docs/reference/editor/overview#options):
+
+```ts
+type resolveFileUrl = (url: string) => Promise;
+```
diff --git a/docs/content/docs/react/components/index.mdx b/docs/content/docs/react/components/index.mdx
new file mode 100644
index 0000000000..1b6e18d3c0
--- /dev/null
+++ b/docs/content/docs/react/components/index.mdx
@@ -0,0 +1,36 @@
+---
+title: UI Components
+description: BlockNote includes a number of UI Components (like menus and toolbars) that can be completely customized.
+---
+
+# UI Components
+
+BlockNote includes a number of UI Components (like menus and toolbars) that can be completely customized:
+
+- [Block Side Menu](/docs/react/components/side-menu)
+- [Formatting Toolbar](/docs/react/components/formatting-toolbar)
+- [Suggestion Menus](/docs/react/components/suggestion-menus)
+ {/* - Link Toolbar */}
+ {/* - [Image Toolbar](/docs/react/components/image-toolbar) */}
+
+
+
+## Configuring Portal Targets
+
+By default, all floating UI elements (toolbars, menus, table handles, etc.) portal into the editor's `bn-container` so they stay scoped to the editor. If your layout needs them to escape — e.g. an `overflow: hidden` ancestor that would clip large dropdowns, or a host modal with its own stacking context — pass a `portalElements` prop to `BlockNoteView`:
+
+```tsx
+
+```
+
+Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. See the [Portal Targets example](/examples/ui-components/portal-elements).
+
+Note: changing `portalElements.default` after mount requires remounting the editor (`editor.mount()` consults it once); per-element keys update reactively.
diff --git a/docs/content/docs/react/components/side-menu.mdx b/docs/content/docs/react/components/side-menu.mdx
new file mode 100644
index 0000000000..cec3bfedc5
--- /dev/null
+++ b/docs/content/docs/react/components/side-menu.mdx
@@ -0,0 +1,52 @@
+---
+title: Block Side Menu
+description: The Block Side Menu appears on the left side whenever you hover a block.
+---
+
+# Block Side Menu
+
+The Block Side Menu appears on the left side whenever you hover a block. By default, it consists of a `+` button and a drag handle (`⠿`):
+
+
+
+Clicking the drag handle (`⠿`) in the Block Side Menu opens the Drag Handle Menu:
+
+
+
+## Changing the Block Side Menu
+
+You can change or replace the Block Side Menu with your own React component. In the demo below, the button to add a new block is replaced with one to remove the hovered block.
+
+
+
+We first define our custom `RemoveBlockButton`. The `useComponentsContext` hook gets all components used internally by BlockNote, so we want to use `Components.SideMenu.Button` for this.
+
+We use the `SideMenu` component to create a custom Block Side Menu. By specifying its children, we can replace the default buttons in the menu with our own.
+
+This custom Side Menu is passed to a `SideMenuController`, which controls its position and visibility (on the left side when you hover a block).
+
+Setting `sideMenu={false}` on `BlockNoteView` tells BlockNote not to show the default Block Side Menu.
+
+## Changing Drag Handle Menu Items
+
+You can also change the items in the Drag Handle Menu. The demo below adds an item that resets the block type to a paragraph.
+
+
+
+Here, we use the `SideMenu` component but keep the default buttons (we don't pass any children). Instead, we pass our customized Drag Handle Menu using the `dragHandleMenu` prop.
diff --git a/docs/content/docs/react/components/suggestion-menus.mdx b/docs/content/docs/react/components/suggestion-menus.mdx
new file mode 100644
index 0000000000..c44f18af45
--- /dev/null
+++ b/docs/content/docs/react/components/suggestion-menus.mdx
@@ -0,0 +1,162 @@
+---
+title: Suggestion Menus
+description: Suggestion Menus appear when the user enters a trigger character, and text after the character is used to filter the menu items.
+---
+
+# Suggestion Menus
+
+Suggestion Menus appear when the user enters a trigger character, and text after the character is used to filter the menu items.
+
+## Slash Menu
+
+The Slash Menu is a Suggestion Menu that opens with the `/` character (or when clicking the `+` button in the [Block Side Menu](/docs/react/components/side-menu).
+
+
+
+### Changing Slash Menu Items
+
+You can change the items in the Slash Menu. The demo below adds an item that inserts a new block, with "Hello World" in bold.
+
+
+
+Slash Menu items are objects with the following fields:
+
+```typescript
+type DefaultSuggestionItem = {
+ title: string;
+ onItemClick: () => void;
+ subtext?: string;
+ badge?: string;
+ aliases?: string[];
+ group?: string;
+};
+```
+
+`title:` The title of the item.
+
+`onItemClick:` A callback function to call when selecting the item.
+
+`subtext:` The subtitle of the item.
+
+`badge:` Text to display in a badge within the item. Intended to show the keyboard shortcut for the item.
+
+`aliases:` Other names for the item other than the `title`, which are used for filtering items based on the user query.
+
+`group:` The group the item belongs to. Items in the same group are separated by a divider in the menu. To ensure items are grouped properly, make sure they are consecutive in the array of items you pass to the Slash Menu.
+
+After creating your item, there are a some changes you must make to `BlockNoteView` to add it to the Slash Menu.
+
+Passing `slashMenu={false}` to `BlockNoteView` tells BlockNote not to show the default Slash Menu. Adding the `SuggestionMenuController` with `triggerCharacter={"/"}` and a custom `getItems` function tells BlockNote to show one with custom items instead.
+
+`getItems` should return the items that need to be shown in the Slash Menu, based on a `query` entered by the user (anything the user types after the `triggerCharacter`). In this case, we simply append the "Hello World" item to the default Slash Menu items, and use `filterSuggestionItems` to filter the full list of items based on the user query.
+
+### Item Grouping & Ordering
+
+Slash Menu items are rendered in the same order as the items returned from `getItems`. Adjacent items which share the same `group` attribute are rendered together in the same group under a single label.
+
+#### Ordering
+
+Items appear in the menu in the exact order of the array. Reordering the array reorders the menu:
+
+```typescript
+getItems={async (query) =>
+ filterSuggestionItems(
+ [
+ insertHelloWorldItem(editor), // Shown first
+ ...getDefaultReactSlashMenuItems(editor), // Shown after
+ ],
+ query,
+ )
+}
+```
+
+#### Grouping
+
+Items with the same `group` attribute must be **adjacent** in the array to be rendered as one group. If items with the same `group` are separated by items with a different `group`, they will be rendered as two separate groups, each with their own label:
+
+```typescript
+// Renders as a single "Basic" group:
+[
+ { title: "Item A", group: "Basic", /* ... */ },
+ { title: "Item B", group: "Basic", /* ... */ },
+ { title: "Item C", group: "Other", /* ... */ },
+]
+
+// Renders as two separate "Basic" groups, with "Other" between them:
+[
+ { title: "Item A", group: "Basic", /* ... */ },
+ { title: "Item C", group: "Other", /* ... */ },
+ { title: "Item B", group: "Basic", /* ... */ },
+]
+```
+
+#### Finding, Inserting, Removing & Reordering Items
+
+Use regular array operations to manipulate items. For example, to insert a custom item directly after the default `Heading 1` item:
+
+```typescript
+const items = getDefaultReactSlashMenuItems(editor);
+const headingIndex = items.findIndex((item) => item.title === "Heading 1");
+items.splice(headingIndex + 1, 0, insertHelloWorldItem(editor));
+```
+
+To remove an item:
+
+```typescript
+const items = getDefaultReactSlashMenuItems(editor).filter(
+ (item) => item.title !== "Heading 1",
+);
+```
+
+To reorder items, sort or rearrange the array however you'd like before returning it from `getItems`.
+
+The demo below combines these techniques to render only the "Basic blocks" and "Headings" groups, with their order swapped:
+
+
+
+### Replacing the Slash Menu Component
+
+You can replace the React component used for the Slash Menu with your own, as you can see in the demo below.
+
+
+
+Again, we add a `SuggestionMenuController` component with `triggerCharacter={"/"}` and set `slashMenu={false}` to replace the default Slash Menu.
+
+Now, we also pass a component to its `suggestionMenuComponent` prop. The `suggestionMenuComponent` we pass is responsible for rendering the filtered items. The `SuggestionMenuController` controls its position and visibility (below the trigger character), and it also determines which items should be shown (using the optional `getItems` prop we've seen above).
+
+## Creating additional Suggestion Menus
+
+You can add additional Suggestion Menus to the editor, which can use any trigger character. The demo below adds an example Suggestion Menu for mentions, which opens with the `@` character.
+
+
+
+Changing the items in the new Suggestion Menu, or the component used to render it, is done the same way as for the [Slash Menu](/docs/react/components/suggestion-menus#slash-menu). For more information about how the mentions elements work, see [Custom Inline Content](/docs/features/custom-schemas/custom-inline-content).
+
+## Additional Features
+
+BlockNote offers a few other features for working with Suggestion Menus which may fit your use case.
+
+### Opening Suggestion Menus Programmatically
+
+While suggestion menus are generally meant to be opened when the user presses a trigger character, you may also want to open them from code. To do this, you can use the following editor method:
+
+```typescript
+openSuggestionMenu(triggerCharacter: string): void;
+
+// Usage
+editor.openSuggestionMenu("/");
+```
+
+### Waiting for a Query
+
+You may want to hold off displaying a Suggestion Menu unless you're certain that the user actually wants to open the menu and not just enter the trigger character. In this case, you should use the `minQueryLength` prop for `SuggestionMenuController`, which takes a number.
+
+The number indicates how many characters the user query needs to have before the menu is shown. When greater than 0, it also prevents the menu from displaying if the user enters a space immediately after the trigger character.
diff --git a/docs/content/docs/react/meta.json b/docs/content/docs/react/meta.json
new file mode 100644
index 0000000000..e3dbaaf2e5
--- /dev/null
+++ b/docs/content/docs/react/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "React",
+ "pages": ["overview", "..."]
+}
diff --git a/docs/content/docs/react/overview.mdx b/docs/content/docs/react/overview.mdx
new file mode 100644
index 0000000000..b85d29ab21
--- /dev/null
+++ b/docs/content/docs/react/overview.mdx
@@ -0,0 +1,146 @@
+---
+title: Overview
+description: Learn how to use BlockNote With React
+imageTitle: Using BlockNote With React
+---
+
+# Using BlockNote With React
+
+BlockNote provides a powerful React integration that makes it easy to add rich text editing capabilities to your applications. The React bindings offer a declarative API that integrates seamlessly with React's component model and state management patterns.
+
+## Key Components
+
+The React integration centers around two main pieces:
+
+- **`useCreateBlockNote`** - A React hook that creates and manages editor instances
+- **`BlockNoteView`** - A component that renders the editor with a complete UI
+
+## Quick Start
+
+Here's a minimal example of how to integrate BlockNote into a React component:
+
+```tsx
+import React from "react";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+// Or, you can use ariakit, shadcn, etc.
+
+function MyEditor() {
+ const editor = useCreateBlockNote();
+
+ return ;
+}
+```
+
+This gives you a fully functional editor with:
+
+- Text editing and formatting
+- Block types (paragraphs, headings, lists, etc.)
+- Toolbar for formatting options
+- Side menu for block operations
+
+## BlockNoteView
+
+The `` component is used to render the editor. It also provides a number of props for editor specific events.
+
+### Props
+
+
+
+## Hooks
+
+### useCreateBlockNote
+
+The `useCreateBlockNote` hook is used to create a new `BlockNoteEditor` instance.
+
+```tsx twoslash
+import React from "react";
+/**
+ * The options for the editor, like initial content, schema, etc.
+ * See the [Editor Options API reference](/docs/reference/editor/options) for more details
+ */
+type BlockNoteEditorOptions = object;
+/**
+ * See the [Editor API reference](/docs/reference/editor/manipulate-blocks) for more details
+ */
+type BlockNoteEditor = object;
+/**
+ * This hook creates a new editor instance, but doesn't render it.
+ */
+// ---cut---
+declare function useCreateBlockNote(
+ options?: BlockNoteEditorOptions,
+ deps?: React.DependencyList,
+): BlockNoteEditor;
+```
+
+### useEditorChange
+
+The `useEditorChange` hook is used to listen for changes to the editor.
+
+```tsx twoslash
+import React from "react";
+/**
+ * The blocks that were inserted, updated, or deleted by the change that occurred.
+ * See the [Events documentation](/docs/reference/editor/events#onchange) for more details
+ */
+type BlocksChanged = object;
+/**
+ * See the [Editor API reference](/docs/reference/editor/manipulate-blocks) for more details
+ */
+type BlockNoteEditor = object;
+/**
+ * This hook creates a new editor instance, but doesn't render it.
+ */
+// ---cut---
+declare function useEditorChange(
+ callback: (
+ editor: BlockNoteEditor,
+ ctx: {
+ /**
+ * Returns the blocks that were inserted, updated, or deleted by the change that occurred.
+ */
+ getChanges(): BlocksChanged;
+ },
+ ) => void,
+ editor?: BlockNoteEditor,
+): BlockNoteEditor;
+```
+
+### useEditorSelectionChange
+
+The `useEditorSelectionChange` hook is used to listen for changes to the editor selection.
+
+```tsx twoslash
+import React from "react";
+/**
+ * See the [Editor API reference](/docs/reference/editor/manipulate-blocks) for more details
+ */
+type BlockNoteEditor = object;
+/**
+ * This hook listens for changes to the editor selection.
+ */
+// ---cut---
+declare function useEditorSelectionChange(
+ /**
+ * Callback that runs when the editor's selection changes.
+ */
+ callback: () => void,
+ editor?: BlockNoteEditor,
+): BlockNoteEditor;
+```
+
+## Next Steps
+
+The editor is now ready to use! Start typing and explore the various block types and formatting options available in the toolbar.
+
+Now that you have a basic editor working, you can explore:
+
+- [Built-in Block Types](/docs/features/blocks) - Learn about what types of content the BlockNote editor supports by default
+- [Styling & Theming](/docs/react/styling-theming) - Customize how the editor looks and feels
+- [Custom UI Elements](/docs/react/components) - Replace the default UI components to really personalize your editor
+- [Custom Schemas](/docs/features/custom-schemas) - Expand the types of content that users can add to the editor
+- [Examples](/examples) - Browse a library of examples created by the BlockNote maintainers and community members
diff --git a/docs/content/docs/react/styling-theming/adding-dom-attributes.mdx b/docs/content/docs/react/styling-theming/adding-dom-attributes.mdx
new file mode 100644
index 0000000000..596b5c7fc1
--- /dev/null
+++ b/docs/content/docs/react/styling-theming/adding-dom-attributes.mdx
@@ -0,0 +1,24 @@
+---
+title: Adding DOM Attributes
+description: BlockNote allows you to change how the editor UI looks. You can change the theme of the default UI, or override its CSS styles.
+---
+
+# Adding DOM Attributes
+
+BlockNote allows you to add custom HTML attributes to various DOM elements within the editor. This gives you fine-grained control over styling and functionality.
+
+## Available DOM Elements
+
+The following DOM elements can receive custom attributes:
+
+- **`editor`**: The main editor container, excluding menus & toolbars
+- **`block`**: The container element for individual blocks
+- **`blockGroup`**: Wrapper for top-level and nested blocks
+- **`blockContent`**: Wrapper for a block's content
+- **`inlineContent`**: Wrapper for rich-text content within blocks
+
+## Example Usage
+
+The demo below shows how to add a custom class to the `block` element to create a border around each block:
+
+
diff --git a/docs/content/docs/react/styling-theming/index.mdx b/docs/content/docs/react/styling-theming/index.mdx
new file mode 100644
index 0000000000..66c2b7aac2
--- /dev/null
+++ b/docs/content/docs/react/styling-theming/index.mdx
@@ -0,0 +1,12 @@
+---
+title: Styling & Theming
+description: You can completely change the look and feel of the BlockNote editor. Change basic styling quickly with theme CSS variables, or apply more complex styles with additional CSS rules.
+---
+
+# Styling & Theming
+
+You can completely change the look and feel of the BlockNote editor. Change basic styling quickly with [theme CSS variables](/docs/react/styling-theming/themes), or apply more complex styles with [additional CSS rules](/docs/react/styling-theming/overriding-css).
+
+If you want to change, remove, or entirely replace the React components for menus & toolbars, see [UI Components](/docs/react/components).
+
+
diff --git a/docs/content/docs/react/styling-theming/meta.json b/docs/content/docs/react/styling-theming/meta.json
new file mode 100644
index 0000000000..cc157a56a0
--- /dev/null
+++ b/docs/content/docs/react/styling-theming/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Styling & Theming",
+ "pages": ["themes", "overriding-css", "adding-dom-attributes", "..."]
+}
diff --git a/docs/content/docs/react/styling-theming/overriding-css.mdx b/docs/content/docs/react/styling-theming/overriding-css.mdx
new file mode 100644
index 0000000000..bece286f4d
--- /dev/null
+++ b/docs/content/docs/react/styling-theming/overriding-css.mdx
@@ -0,0 +1,44 @@
+---
+title: Overriding CSS
+description: You can change any styles applied to the editor by setting your own CSS styles.
+---
+
+# Overriding CSS
+
+BlockNote provides several ways to customize the editor's appearance through CSS. You can override default styles using CSS classes and attributes.
+
+## Basic Example
+
+In the demo below, we create additional CSS rules to make the editor text and hovered slash menu items blue:
+
+
+
+## CSS Selectors Reference
+
+### BlockNote CSS Classes
+
+BlockNote uses classes with the `bn-` prefix to style editor elements. Here are the key classes you can target:
+
+#### Editor Structure
+
+- `.bn-root`: Container class both the floating menus / toolbars and the editor
+- `.bn-container`: Container around `.bn-editor`
+- `.bn-editor`: Main editor element (the "contenteditable").
+- `.bn-block`: Individual block element (including nested).
+- `.bn-block-group`: Container for nested blocks.
+- `.bn-block-content`: Block content wrapper.
+- `.bn-inline-content`: Block's editable rich text content.
+
+#### UI Components
+
+- `.bn-toolbar`: Formatting & link toolbars.
+- `.bn-side-menu`: Side menu element.
+- `.bn-drag-handle-menu`: Drag handle menu.
+- `.bn-suggestion-menu`: Suggestion menu.
+
+### BlockNote CSS Attributes
+
+BlockNote uses data attributes to target specific block types and properties:
+
+- `[data-content-type="blockType"]`: Targets blocks of type `blockType`.
+- `[data-propName="propValue"]`: Targets blocks with specific prop values. If the value is the same as the default value, the `data-propName` attribute will not be added.
diff --git a/docs/content/docs/react/styling-theming/themes.mdx b/docs/content/docs/react/styling-theming/themes.mdx
new file mode 100644
index 0000000000..5965818ca0
--- /dev/null
+++ b/docs/content/docs/react/styling-theming/themes.mdx
@@ -0,0 +1,140 @@
+---
+title: Themes
+description: Themes let you quickly change the basic look of the editor UI, including colors, borders, shadows, and font.
+---
+
+# Themes
+
+BlockNote comes with both a light and dark theme. By default, the theme is automatically selected based on the user's system preference, but you can also force either light or dark mode.
+
+When [using Mantine](/docs/getting-started/mantine), you have additional theme functionality:
+
+- Custom color schemes for light and dark mode, including highlight colors
+- Change fonts, shadows, borders, and border radii
+- Custoimize themes programmatically or using CSS variable overrides
+
+This page mostly focuses on the extra functionality that comes from using Mantine, but the [Forcing Light/Dark Mode](/docs/react/styling-theming/themes#forcing-lightdark-mode) section is applicable to all UI libraries.
+
+
+
+## CSS Variables
+
+A theme is made up of a set of CSS variables, which can be overwritten to change the editor theme.
+
+Here are each of the theme CSS variables you can set, with values from the default light theme:
+
+```css
+--bn-colors-editor-text: #3f3f3f;
+--bn-colors-editor-background: #ffffff;
+--bn-colors-menu-text: #3f3f3f;
+--bn-colors-menu-background: #ffffff;
+--bn-colors-tooltip-text: #3f3f3f;
+--bn-colors-tooltip-background: #efefef;
+--bn-colors-hovered-text: #3f3f3f;
+--bn-colors-hovered-background: #efefef;
+--bn-colors-selected-text: #ffffff;
+--bn-colors-selected-background: #3f3f3f;
+--bn-colors-disabled-text: #afafaf;
+--bn-colors-disabled-background: #efefef;
+
+--bn-colors-shadow: #cfcfcf;
+--bn-colors-border: #efefef;
+--bn-colors-side-menu: #cfcfcf;
+
+--bn-colors-highlights-gray-text: #9b9a97;
+--bn-colors-highlights-gray-background: #ebeced;
+--bn-colors-highlights-brown-text: #64473a;
+--bn-colors-highlights-brown-background: #e9e5e3;
+--bn-colors-highlights-red-text: #e03e3e;
+--bn-colors-highlights-red-background: #fbe4e4;
+--bn-colors-highlights-orange-text: #d9730d;
+--bn-colors-highlights-orange-background: #f6e9d9;
+--bn-colors-highlights-yellow-text: #dfab01;
+--bn-colors-highlights-yellow-background: #fbf3db;
+--bn-colors-highlights-green-text: #4d6461;
+--bn-colors-highlights-green-background: #ddedea;
+--bn-colors-highlights-blue-text: #0b6e99;
+--bn-colors-highlights-blue-background: #ddebf1;
+--bn-colors-highlights-purple-text: #6940a5;
+--bn-colors-highlights-purple-background: #eae4f2;
+--bn-colors-highlights-pink-text: #ad1a72;
+--bn-colors-highlights-pink-background: #f4dfeb;
+
+--bn-font-family:
+ "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Open Sans",
+ "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
+ "Droid Sans", "Helvetica Neue", sans-serif;
+--bn-border-radius: 6px;
+```
+
+Setting these variables on the `.bn-root[data-color-scheme]` selector will overwrite them for both default light & dark themes. To overwrite variables separately for light & dark themes, use the `.bn-root[data-color-scheme="light"]` and `.bn-root[data-color-scheme="dark"]` selectors.
+
+## Programmatic Configuration
+
+You can also set the theme CSS variables using the [`theme` prop in `BlockNoteView`](/docs/react/overview#props). Passing a `Theme` object will overwrite CSS variables for both light & dark themes with values from the object.
+
+```ts twoslash
+/**
+ * A foreground & background pair
+ */
+type CombinedColor = Partial<{
+ text: string;
+ background: string;
+}>;
+
+/**
+ * A color scheme
+ */
+type ColorScheme = Partial<{
+ editor: CombinedColor;
+ menu: CombinedColor;
+ tooltip: CombinedColor;
+ hovered: CombinedColor;
+ selected: CombinedColor;
+ disabled: CombinedColor;
+ shadow: string;
+ border: string;
+ sideMenu: string;
+ highlights: Partial<{
+ gray: CombinedColor;
+ brown: CombinedColor;
+ red: CombinedColor;
+ orange: CombinedColor;
+ yellow: CombinedColor;
+ green: CombinedColor;
+ blue: CombinedColor;
+ purple: CombinedColor;
+ pink: CombinedColor;
+ }>;
+}>;
+
+/**
+ * A theme
+ */
+type Theme = Partial<{
+ colors: ColorScheme;
+ borderRadius: number;
+ fontFamily: string;
+}>;
+```
+
+In the demo below, we create the same red theme as from the previous demo, but this time we set it using the `theme` prop in `BlockNoteView`:
+
+
+
+## Light and Dark Themes
+
+Alternatively, you can overwrite CSS variables for the light & dark theme separately by passing the following object type:
+
+```ts
+type LightAndDarkThemes = {
+ light: Theme;
+ dark: Theme;
+};
+```
+
+## Forcing Light/Dark Mode
+
+By passing `"light"` or `"dark"` to the `theme` prop instead of a `Theme` object, you can also force BlockNote to always use the light or dark theme.
+
+If you want to set more complex styles on the editor, see [Overriding CSS](/docs/react/styling-theming/overriding-css).
diff --git a/docs/content/docs/reference/editor/cursor-selections.mdx b/docs/content/docs/reference/editor/cursor-selections.mdx
new file mode 100644
index 0000000000..9d7c51ac48
--- /dev/null
+++ b/docs/content/docs/reference/editor/cursor-selections.mdx
@@ -0,0 +1,113 @@
+---
+title: Cursor & Selections
+description: Handle cursor positions and text selections in the editor
+---
+
+# Cursor & Selections
+
+BlockNote provides APIs to work with cursor positions and text selections, allowing you to understand where users are interacting with the editor and programmatically control the selection state.
+
+## Text Cursor
+
+The text cursor represents the blinking vertical line where users type. BlockNote provides detailed information about the cursor's position and surrounding context.
+
+### TextCursorPosition Type
+
+```typescript
+type TextCursorPosition = {
+ block: Block;
+ prevBlock: Block | undefined;
+ nextBlock: Block | undefined;
+ parentBlock: Block | undefined;
+};
+```
+
+- `block`: The block currently containing the text cursor
+- `prevBlock`: The previous block at the same nesting level (undefined if first)
+- `nextBlock`: The next block at the same nesting level (undefined if last)
+- `parentBlock`: The parent block if the cursor is in a nested block (undefined if at the top level)
+
+### Getting Cursor Position
+
+```typescript
+getTextCursorPosition(): TextCursorPosition;
+
+// Usage
+const cursorPos = editor.getTextCursorPosition();
+console.log("Cursor is in block:", cursorPos.block.id);
+```
+
+### Setting Cursor Position
+
+```typescript
+setTextCursorPosition(
+ targetBlock: BlockIdentifier,
+ placement: "start" | "end" = "start"
+): void;
+
+// Usage
+editor.setTextCursorPosition(blockId, "start");
+editor.setTextCursorPosition(blockId, "end");
+```
+
+**Parameters:**
+
+- `targetBlock`: Block ID or Block object to position cursor in
+- `placement`: Whether to place cursor at start or end of block
+
+**Throws:** Error if target block doesn't exist
+
+## Selections
+
+Selections represent highlighted content spanning multiple blocks. BlockNote provides APIs to get and set selections programmatically.
+
+### Selection Type
+
+```typescript
+type Selection = {
+ blocks: Block[];
+};
+```
+
+- `blocks`: Array of all blocks spanned by the selection (including nested blocks)
+
+### Getting Current Selection
+
+```typescript
+getSelection(): Selection | undefined;
+
+// Usage
+const selection = editor.getSelection();
+if (selection) {
+ console.log("Selected blocks:", selection.blocks.length);
+}
+```
+
+**Returns:** Current selection or `undefined` if no selection is active
+
+### Setting Selection
+
+```typescript
+setSelection(startBlock: BlockIdentifier, endBlock: BlockIdentifier): void;
+
+// Usage
+editor.setSelection(startBlockId, endBlockId);
+```
+
+**Parameters:**
+
+- `startBlock`: Block where selection should begin
+- `endBlock`: Block where selection should end
+
+**Requirements:**
+
+- Both blocks must have content
+- Selection spans from start of first block to end of last block
+
+**Throws:** Error if blocks don't exist or have no content
+
+## Related APIs
+
+- **[Manipulating Blocks](/docs/reference/editor/manipulating-content#block-manipulation)** - Work with selected blocks
+- **[Manipulating Inline Content](/docs/reference/editor/manipulating-content#inline-content-manipulation)** - Format selected text
+- **[Events](/docs/reference/editor/events)** - Listen for selection changes
diff --git a/docs/content/docs/reference/editor/events.mdx b/docs/content/docs/reference/editor/events.mdx
new file mode 100644
index 0000000000..137c479b55
--- /dev/null
+++ b/docs/content/docs/reference/editor/events.mdx
@@ -0,0 +1,177 @@
+---
+title: Events
+description: BlockNote emits events when certain actions occur in the editor
+---
+
+# Events
+
+BlockNote provides several event callbacks that allow you to respond to changes in the editor. These events are essential for building reactive applications and tracking user interactions.
+
+## Overview
+
+The editor emits events for:
+
+- **Editor lifecycle** - When the editor is created, mounted, unmounted, etc.
+- **Content changes** - When blocks are inserted, updated, or deleted
+- **Selection changes** - When the cursor position or selection changes
+
+## `onMount`
+
+The `onMount` callback is called when the editor has been mounted.
+
+```typescript
+editor.onMount(() => {
+ console.log("Editor is mounted");
+});
+```
+
+## `onUnmount`
+
+The `onUnmount` callback is called when the editor has been unmounted.
+
+```typescript
+editor.onUnmount(() => {
+ console.log("Editor is unmounted");
+});
+```
+
+## `onSelectionChange`
+
+The `onSelectionChange` callback is called whenever the editor's selection changes, including cursor movements and text selections.
+
+```typescript
+editor.onSelectionChange((editor) => {
+ console.log("Selection changed");
+
+ // Get current selection information
+ const selection = editor.getSelection();
+ const textCursorPosition = editor.getTextCursorPosition();
+
+ console.log("Current selection:", selection);
+ console.log("Text cursor position:", textCursorPosition);
+});
+```
+
+## `onChange`
+
+The `onChange` callback is called whenever the editor's content changes. This is the primary way to track modifications to the document.
+
+```typescript
+editor.onChange((editor, { getChanges }) => {
+ console.log("Editor content changed");
+
+ // Get detailed information about what changed
+ const changes = getChanges();
+ console.log("Changes:", changes);
+
+ // Save content, update UI, etc.
+});
+```
+
+See [Understanding Changes](#understanding-changes) for more information about the `getChanges` function.
+
+## `onBeforeChange`
+
+The `onBeforeChange` callback is called before any change is applied to the editor, allowing you to cancel the change.
+
+```typescript
+editor.onBeforeChange(({ getChanges, tr }) => {
+ if (
+ // Cancel inserting new blocks
+ getChanges().some((change) => change.type === "insert")
+ ) {
+ // By returning `false`, the change will be canceled & not applied to the editor.
+ return false;
+ }
+});
+```
+
+See [Understanding Changes](#understanding-changes) for more information about the `getChanges` function.
+
+## Understanding Changes
+
+The `getChanges()` function returns detailed information about what blocks were affected. It includes three types of changes:
+
+- Insertions - When a new block is inserted
+- Deletions - When a block is deleted
+- Updates - When a block's content is changed
+- Moves - When a block is moved to a new position (which can also update the block's content)
+
+```typescript
+/**
+ * The changes that occurred in the editor.
+ */
+type BlocksChanged = Array<
+ | {
+ type: "insert" | "delete";
+ // The affected block (when inserting, this is the new block, when deleting, this is the block that was deleted)
+ block: Block;
+ // The source of the change
+ source: BlockChangeSource;
+ // Insert and delete changes don't have a previous block
+ prevBlock: undefined;
+ }
+ | {
+ type: "update";
+ // The affected block
+ block: Block;
+ // The source of the change
+ source: BlockChangeSource;
+ // The block before the update
+ prevBlock: Block;
+ }
+ | {
+ type: "move";
+ // The source of the change
+ source: BlockChangeSource;
+ // The affected block
+ block: Block;
+ // The block before the move (since a move can also update the block's content)
+ prevBlock: Block;
+ /**
+ * The previous parent block (if it existed).
+ */
+ prevParent?: Block;
+ /**
+ * The current parent block (if it exists).
+ */
+ currentParent?: Block;
+ }
+>;
+```
+
+### Change Sources
+
+Each change includes a source that indicates what triggered the modification:
+
+```typescript
+type BlockChangeSource = {
+ type:
+ | "local" // Triggered by local user (default)
+ | "paste" // From paste operation
+ | "drop" // From drop operation
+ | "undo" // From undo operation (local-only)
+ | "redo" // From redo operation (local-only)
+ | "undo-redo" // From undo/redo operations (collaboration-only)
+ | "yjs-remote"; // From remote user (collaboration-only)
+};
+```
+
+## Event Cleanup
+
+All event callbacks return cleanup functions that you can call to remove the event listener:
+
+```typescript
+// Set up event listeners
+const cleanupOnChange = editor.onChange((editor, { getChanges }) => {
+ console.log("Content changed");
+});
+
+const cleanupOnSelection = editor.onSelectionChange((editor) => {
+ console.log("Selection changed");
+});
+
+// Later, clean up event listeners
+cleanupOnChange();
+cleanupOnSelection();
+```
diff --git a/docs/content/docs/reference/editor/low-level.mdx.bak b/docs/content/docs/reference/editor/low-level.mdx.bak
new file mode 100644
index 0000000000..e11e891e72
--- /dev/null
+++ b/docs/content/docs/reference/editor/low-level.mdx.bak
@@ -0,0 +1,384 @@
+---
+title: Low-level APIs
+description: Advanced APIs for direct editor state manipulation and ProseMirror integration
+---
+
+# Low-level APIs
+
+BlockNote provides low-level APIs for advanced use cases that require direct access to the underlying ProseMirror editor state and transactions. These APIs are primarily intended for:
+
+- **Reading editor state** (document, selection, etc.)
+- **Performing complex operations** that need to be batched together
+- **ProseMirror ecosystem compatibility** for existing plugins and extensions
+
+## Core Concepts
+
+### BlockNote Transactions
+
+The `transact` method is the primary way to interact with the editor's low-level state. It provides a safe way to read the current state and perform changes while ensuring proper batching and undo/redo behavior.
+
+### When to Use Each API
+
+- **`transact`**: Primary API for reading state and performing changes. Use this for most low-level operations.
+- **`exec`**: For ProseMirror ecosystem compatibility. Avoid using this for BlockNote extensions.
+- **`canExec`**: For checking if ProseMirror commands can be executed. Avoid using this for BlockNote extensions.
+
+## The `transact` Method
+
+The `transact` method is the foundation for low-level editor operations. It provides a ProseMirror transaction object that allows you to read the current state and perform changes.
+
+### Basic Usage
+
+```typescript
+editor.transact((tr) => {
+ // Read state
+ const doc = tr.doc;
+ const selection = tr.selection;
+
+ // Perform changes
+ tr.insertText("Hello, world!");
+
+ // Return values
+ return { docSize: doc.content.size };
+});
+```
+
+### Reading Editor State
+
+You can read various aspects of the editor state within a transaction:
+
+```typescript
+// Get document information
+const docInfo = editor.transact((tr) => {
+ return {
+ totalSize: tr.doc.content.size,
+ isSelectionEmpty: tr.selection.empty,
+ selectionFrom: tr.selection.from,
+ selectionTo: tr.selection.to,
+ };
+});
+
+console.log(`Document has ${docInfo.totalSize} characters`);
+console.log(`Selection is ${docInfo.isSelectionEmpty ? "empty" : "not empty"}`);
+```
+
+### Reading Selection Information
+
+```typescript
+const selectionInfo = editor.transact((tr) => {
+ const { selection } = tr;
+
+ return {
+ isEmpty: selection.empty,
+ from: selection.from,
+ to: selection.to,
+ anchor: selection.anchor,
+ head: selection.head,
+ // Get the text content of the selection
+ selectedText: tr.doc.textBetween(selection.from, selection.to),
+ };
+});
+```
+
+### Reading Document Structure
+
+```typescript
+const documentStructure = editor.transact((tr) => {
+ const doc = tr.doc;
+ const blocks: Array<{ type: string; pos: number }> = [];
+
+ doc.descendants((node, pos) => {
+ if (node.type.name === "blockContainer") {
+ blocks.push({
+ type: node.attrs.blockType || "unknown",
+ pos: pos,
+ });
+ }
+ });
+
+ return blocks;
+});
+```
+
+### Performing Multiple Operations
+
+The `transact` method automatically batches all operations into a single undo/redo step:
+
+```typescript
+editor.transact((tr) => {
+ // All these operations will be grouped together
+ tr.insertText("First operation");
+ tr.insertText("Second operation");
+ tr.insertText("Third operation");
+
+ // This creates only one undo step
+});
+```
+
+### Nested Transactions
+
+You can nest `transact` calls, and they will all use the same underlying transaction:
+
+```typescript
+editor.transact((tr) => {
+ tr.insertText("Start");
+
+ // This nested transact uses the same transaction
+ editor.transact((nestedTr) => {
+ nestedTr.insertText("Nested");
+ });
+
+ tr.insertText("End");
+
+ // All operations are still batched together
+});
+```
+
+### Returning Values
+
+The `transact` method returns whatever value you return from the callback:
+
+```typescript
+const result = editor.transact((tr) => {
+ const docSize = tr.doc.content.size;
+ const selectionSize = tr.selection.to - tr.selection.from;
+
+ // Perform some operations
+ tr.insertText("Modified content");
+
+ // Return computed values
+ return {
+ originalSize: docSize,
+ originalSelectionSize: selectionSize,
+ newSize: tr.doc.content.size,
+ };
+});
+
+console.log(
+ `Document grew by ${result.newSize - result.originalSize} characters`,
+);
+```
+
+### Reading and Modifying in One Transaction
+
+```typescript
+const modificationResult = editor.transact((tr) => {
+ // Read current state
+ const originalText = tr.doc.textBetween(tr.selection.from, tr.selection.to);
+ const originalLength = originalText.length;
+
+ // Perform modifications
+ tr.insertText("New content");
+
+ // Read modified state
+ const newText = tr.doc.textBetween(tr.selection.from, tr.selection.to);
+ const newLength = newText.length;
+
+ return {
+ originalText,
+ originalLength,
+ newText,
+ newLength,
+ change: newLength - originalLength,
+ };
+});
+```
+
+## The `exec` Method
+
+The `exec` method executes ProseMirror commands. This is primarily for compatibility with the ProseMirror ecosystem and should not be used for BlockNote extensions.
+
+### Basic Usage
+
+```typescript
+editor.exec((state, dispatch, view) => {
+ if (dispatch) {
+ dispatch(state.tr.insertText("Hello, world!"));
+ }
+ return true;
+});
+```
+
+### Checking Before Executing
+
+An early return can be used to check whether a command can be executed. This pattern is useful for determining whether a command can be executed before actually executing it.
+
+```typescript
+const canInsertText = editor.exec((state, dispatch, view) => {
+ if (!state.selection.empty) {
+ return false;
+ }
+
+ if (dispatch) {
+ dispatch(state.tr.insertText("Inserted text"));
+ }
+ return true;
+});
+```
+
+### Important Notes
+
+- **Cannot be used within `transact`**: The `exec` method conflicts with `transact` calls
+- **Prefer `transact`**: Use `transact` for most operations as it provides better integration with BlockNote
+- **Recommendation**: Only use `exec` when working with existing ProseMirror plugins or commands
+
+## The `canExec` Method
+
+The `canExec` method checks whether a ProseMirror command can be executed without actually executing it.
+
+### Basic Usage
+
+```typescript
+const canReplaceSelection = editor.canExec((state, dispatch, view) => {
+ // Check if there's a selection to replace
+ if (state.selection.from === state.selection.to) {
+ return false;
+ }
+
+ if (dispatch) {
+ dispatch(state.tr.insertText("Replacement text"));
+ }
+ return true;
+});
+
+if (canReplaceSelection) {
+ console.log("Can replace current selection");
+} else {
+ console.log("No selection to replace");
+}
+```
+
+### Important Notes
+
+- **Cannot be used within `transact`**: The `canExec` method conflicts with `transact` calls
+- **Prefer `transact`**: Use `transact` for reading state when possible
+- **Recommendation**: Only use `canExec` when working with existing ProseMirror plugins or commands
+
+## Best Practices
+
+### 1. Use `transact` for Most Operations
+
+```typescript
+// ✅ Good - Using transact
+editor.transact((tr) => {
+ const canInsert = tr.selection.empty;
+ if (canInsert) {
+ tr.insertText("Text");
+ }
+ return canInsert;
+});
+
+// ❌ Avoid - Using exec for simple operations
+editor.exec((state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.insertText("Text"));
+ }
+ return true;
+});
+```
+
+### 2. Batch Related Operations
+
+```typescript
+// ✅ Good - Batching related operations
+editor.transact((tr) => {
+ tr.insertText("First");
+ tr.insertText("Second");
+ tr.insertText("Third");
+ // All operations are batched together
+});
+
+// ❌ Avoid - Multiple separate operations
+editor.transact((tr) => tr.insertText("First"));
+editor.transact((tr) => tr.insertText("Second"));
+editor.transact((tr) => tr.insertText("Third"));
+// Creates multiple undo steps
+```
+
+### 3. Read State Before Modifying
+
+```typescript
+// ✅ Good - Reading state before modifying
+editor.transact((tr) => {
+ const originalSelection = {
+ from: tr.selection.from,
+ to: tr.selection.to,
+ };
+
+ tr.insertText("New content");
+
+ return {
+ originalSelection,
+ newSelection: {
+ from: tr.selection.from,
+ to: tr.selection.to,
+ },
+ };
+});
+```
+
+## Advanced Examples
+
+### Custom Selection Manipulation
+
+```typescript
+const expandSelection = editor.transact((tr) => {
+ const { selection } = tr;
+ const { from, to } = selection;
+
+ // Expand selection by 5 characters in each direction
+ const newFrom = Math.max(0, from - 5);
+ const newTo = Math.min(tr.doc.content.size, to + 5);
+
+ tr.setSelection(TextSelection.create(tr.doc, newFrom, newTo));
+
+ return {
+ originalRange: { from, to },
+ newRange: { from: newFrom, to: newTo },
+ };
+});
+```
+
+### Document Analysis
+
+```typescript
+const generateTableOfContents = editor.transact((tr) => {
+ const doc = tr.doc;
+ const toc: Array<{
+ level: number;
+ text: string;
+ position: number;
+ id?: string;
+ }> = [];
+
+ doc.descendants((node, pos) => {
+ if (node.type.name === "heading") {
+ // Extract heading level from the heading block
+ const level = node.attrs.level || 1;
+
+ // Get the text content of the heading
+ const text = node.textContent;
+
+ // Get the block ID if available
+ const id = node.attrs.id;
+
+ toc.push({
+ level,
+ text,
+ position: pos,
+ id,
+ });
+ }
+ });
+
+ // Sort by position to maintain document order
+ toc.sort((a, b) => a.position - b.position);
+
+ return {
+ totalHeadings: toc.length,
+ tableOfContents: toc,
+ };
+});
+```
+
+These low-level APIs provide powerful tools for advanced editor customization while maintaining proper state management and undo/redo behavior. Always prefer `transact` for most operations, and only use `exec` and `canExec` when working with existing ProseMirror ecosystem code.
diff --git a/docs/content/docs/reference/editor/manipulating-content.mdx b/docs/content/docs/reference/editor/manipulating-content.mdx
new file mode 100644
index 0000000000..1a9c97c222
--- /dev/null
+++ b/docs/content/docs/reference/editor/manipulating-content.mdx
@@ -0,0 +1,512 @@
+---
+title: Manipulating Content
+description: How to read, create, update, and remove blocks and inline content in the BlockNote editor
+---
+
+# Manipulating Content
+
+BlockNote provides comprehensive APIs for manipulating both blocks and inline content within the editor. This guide covers how to programmatically work with the document structure (blocks) and the content within those blocks (text, links, styling).
+
+## Overview
+
+The content manipulation APIs fall into two main categories:
+
+### Block Manipulation
+
+- **[Reading blocks](#reading-blocks)** - Accessing existing blocks and their relationships
+- **[Creating blocks](#creating-blocks)** - Inserting new blocks into the document
+- **[Updating blocks](#updating-blocks)** - Modifying existing block content and properties
+- **[Removing blocks](#removing-blocks)** - Deleting blocks from the document
+- **[Replacing blocks](#replacing-blocks)** - Swapping existing blocks with new ones
+- **[Moving blocks](#moving-blocks)** - Reordering blocks within the document
+- **[Nesting blocks](#nesting-blocks)** - Creating hierarchical relationships between blocks
+
+### Inline Content Manipulation
+
+- **[Inserting content](#inserting-inline-content)** - Adding text, links, and styled content
+- **[Reading content](#reading-content)** - Getting selected text and active styles
+- **[Styling text](#styling-text)** - Adding, removing, and toggling text styles
+- **[Working with links](#working-with-links)** - Creating and accessing link content
+
+## Common Types
+
+### Block Identifiers
+
+Most block methods require a `BlockIdentifier` to reference existing blocks:
+
+```typescript
+type BlockIdentifier = string | { id: string };
+```
+
+You can pass either:
+
+- A `string` representing the block ID
+- Any object with an `id: string` property, such as a `Block`
+
+### Partial Blocks
+
+When creating or updating blocks, you use `PartialBlock` objects which have optional properties:
+
+```typescript
+type PartialBlock = {
+ id?: string; // Auto-generated if not provided
+ type?: string; // Block type (paragraph, heading, etc.)
+ props?: Partial>; // Block-specific properties
+ content?: string | InlineContent[] | TableContent; // Block content
+ children?: PartialBlock[]; // Nested blocks
+};
+```
+
+### Partial Inline Content
+
+When creating or updating inline content, you use `PartialInlineContent` which allows for flexible content specification:
+
+```typescript
+type PartialLink = {
+ type: "link";
+ content: string | StyledText[];
+ href: string;
+};
+
+type PartialInlineContent = string | (string | PartialLink | StyledText)[];
+```
+
+This type allows you to:
+
+- Pass a simple string for plain text
+- Pass an array of mixed content (strings, links, styled text)
+- Use `PartialLink` for link content
+- Use `StyledText` for text with formatting
+
+## Block Manipulation
+
+### Reading Blocks
+
+#### Getting the Document
+
+Retrieve all top-level blocks in the editor:
+
+```typescript
+const blocks = editor.document;
+```
+
+Returns a snapshot of all top-level (non-nested) blocks in the document.
+
+#### Getting Specific Blocks
+
+```typescript
+// Single block
+getBlock(blockIdentifier: BlockIdentifier): Block | undefined
+
+// Previous block
+getPrevBlock(blockIdentifier: BlockIdentifier): Block | undefined
+
+// Next block
+getNextBlock(blockIdentifier: BlockIdentifier): Block | undefined
+
+// Parent block
+getParentBlock(blockIdentifier: BlockIdentifier): Block | undefined
+```
+
+```typescript
+const block = editor.getBlock("block-123");
+const prevBlock = editor.getPrevBlock("block-123");
+const nextBlock = editor.getNextBlock("block-123");
+const parentBlock = editor.getParentBlock("nested-block-123");
+```
+
+#### Traversing All Blocks
+
+```typescript
+forEachBlock(
+ callback: (block: Block) => boolean,
+ reverse: boolean = false
+): void
+```
+
+Traverses all blocks depth-first and executes a callback for each.
+
+```typescript
+editor.forEachBlock((block) => {
+ console.log(`Block ${block.id}: ${block.type}`);
+ return true; // Continue traversal
+});
+```
+
+### Creating Blocks
+
+#### Inserting Blocks
+
+```typescript
+insertBlocks(
+ blocksToInsert: PartialBlock[],
+ referenceBlock: BlockIdentifier,
+ placement: "before" | "after" = "before"
+): void
+```
+
+Inserts new blocks relative to an existing block.
+
+```typescript
+// Insert a paragraph before an existing block
+editor.insertBlocks(
+ [{ type: "paragraph", content: "New paragraph" }],
+ "existing-block-id",
+ "before",
+);
+
+// Insert multiple blocks after an existing block
+editor.insertBlocks(
+ [
+ { type: "heading", content: "New Section", props: { level: 2 } },
+ { type: "paragraph", content: "Section content" },
+ ],
+ "existing-block-id",
+ "after",
+);
+```
+
+### Updating Blocks
+
+#### Modifying Existing Blocks
+
+```typescript
+updateBlock(
+ blockToUpdate: BlockIdentifier,
+ update: PartialBlock
+): void
+```
+
+Updates an existing block with new properties.
+
+```typescript
+// Change a paragraph to a heading
+editor.updateBlock("block-123", {
+ type: "heading",
+ props: { level: 2 },
+});
+
+// Update content only
+editor.updateBlock("block-123", {
+ content: "Updated content",
+});
+
+// Update multiple properties
+editor.updateBlock("block-123", {
+ type: "heading",
+ content: "New heading text",
+ props: { level: 1 },
+});
+```
+
+### Removing Blocks
+
+#### Deleting Blocks
+
+```typescript
+removeBlocks(blocksToRemove: BlockIdentifier[]): void
+```
+
+Removes one or more blocks from the document.
+
+```typescript
+// Remove a single block
+editor.removeBlocks(["block-123"]);
+
+// Remove multiple blocks
+editor.removeBlocks(["block-123", "block-456", "block-789"]);
+```
+
+### Replacing Blocks
+
+#### Swapping Blocks
+
+```typescript
+replaceBlocks(
+ blocksToRemove: BlockIdentifier[],
+ blocksToInsert: PartialBlock[]
+): void
+```
+
+Replaces existing blocks with new ones.
+
+```typescript
+// Replace a paragraph with a heading
+editor.replaceBlocks(
+ ["paragraph-block"],
+ [{ type: "heading", content: "New Heading", props: { level: 2 } }],
+);
+
+// Replace multiple blocks with different content
+editor.replaceBlocks(
+ ["block-1", "block-2"],
+ [
+ { type: "paragraph", content: "Replacement content" },
+ { type: "bulletListItem", content: "List item" },
+ ],
+);
+```
+
+### Moving Blocks
+
+#### Reordering Blocks
+
+```typescript
+moveBlocksUp(blockIdentifier?: BlockIdentifier): void
+moveBlocksDown(blockIdentifier?: BlockIdentifier): void
+```
+
+Moves the currently selected blocks up or down in the document. If a
+`blockIdentifier` is provided, that block is moved instead of the selection,
+and the selection is left unchanged.
+
+```typescript
+// Move selected blocks up
+editor.moveBlocksUp();
+
+// Move selected blocks down
+editor.moveBlocksDown();
+
+// Move a specific block up, without changing the selection
+editor.moveBlocksUp("block-123");
+
+// Move a specific block down, without changing the selection
+editor.moveBlocksDown("block-123");
+```
+
+### Nesting Blocks
+
+#### Creating Hierarchical Structures
+
+```typescript
+canNestBlock(): boolean
+nestBlock(): void
+canUnnestBlock(): boolean
+unnestBlock(): void
+```
+
+Manages the nesting level of blocks (indentation).
+
+```typescript
+// Check if current block can be nested
+if (editor.canNestBlock()) {
+ editor.nestBlock(); // Indent the block
+}
+
+// Check if current block can be un-nested
+if (editor.canUnnestBlock()) {
+ editor.unnestBlock(); // Outdent the block
+}
+```
+
+## Inline Content Manipulation
+
+### Inserting Inline Content
+
+#### Basic Insertion
+
+```typescript
+insertInlineContent(
+ content: PartialInlineContent,
+ options?: { updateSelection?: boolean }
+): void
+```
+
+Inserts content at the current cursor position or replaces the current selection.
+
+```typescript
+// Insert plain text
+editor.insertInlineContent("Hello, world!");
+
+// Insert mixed content
+editor.insertInlineContent([
+ "Hello ",
+ { type: "text", text: "World", styles: { bold: true } },
+ "! Welcome to ",
+ { type: "link", content: "BlockNote", href: "https://blocknotejs.org" },
+]);
+
+// Insert with selection update
+editor.insertInlineContent("New content", { updateSelection: true });
+```
+
+#### Advanced Content Examples
+
+```typescript
+// Insert styled text
+editor.insertInlineContent([
+ {
+ type: "text",
+ text: "Bold and italic",
+ styles: { bold: true, italic: true },
+ },
+]);
+
+// Insert link with styled content
+editor.insertInlineContent([
+ {
+ type: "link",
+ content: [
+ { type: "text", text: "Visit ", styles: {} },
+ { type: "text", text: "BlockNote", styles: { bold: true } },
+ ],
+ href: "https://blocknotejs.org",
+ },
+]);
+
+// Insert complex mixed content
+editor.insertInlineContent([
+ "This is ",
+ { type: "text", text: "important", styles: { bold: true, textColor: "red" } },
+ " and you should ",
+ { type: "link", content: "read more", href: "https://example.com" },
+ " about it.",
+]);
+```
+
+### Reading Content
+
+#### Getting Selected Text
+
+```typescript
+getSelectedText(): string
+```
+
+Retrieves the currently selected text as a plain string.
+
+```typescript
+const selectedText = editor.getSelectedText();
+console.log("Selected text:", selectedText);
+
+// Example: Copy selected text to clipboard
+if (selectedText) {
+ navigator.clipboard.writeText(selectedText);
+}
+```
+
+#### Getting Active Styles
+
+```typescript
+getActiveStyles(): Styles
+```
+
+Returns the active text styles at the current cursor position or at the end of the current selection.
+
+```typescript
+const activeStyles = editor.getActiveStyles();
+console.log("Active styles:", activeStyles);
+
+// Example: Check if text is bold
+if (activeStyles.bold) {
+ console.log("Text is bold");
+}
+
+// Example: Get text color
+if (activeStyles.textColor) {
+ console.log("Text color:", activeStyles.textColor);
+}
+```
+
+#### Getting Selected Link
+
+```typescript
+getSelectedLinkUrl(): string | undefined
+```
+
+Returns the URL of the last link in the current selection, or `undefined` if no links are selected.
+
+```typescript
+const linkUrl = editor.getSelectedLinkUrl();
+
+if (linkUrl) {
+ console.log("Selected link URL:", linkUrl);
+ // Open link in new tab
+ window.open(linkUrl, "_blank");
+} else {
+ console.log("No link selected");
+}
+```
+
+### Styling Text
+
+#### Adding Styles
+
+```typescript
+addStyles(styles: Styles): void
+```
+
+Applies styles to the currently selected text.
+
+```typescript
+// Add single style
+editor.addStyles({ bold: true });
+
+// Add multiple styles
+editor.addStyles({
+ bold: true,
+ italic: true,
+ textColor: "red",
+});
+
+// Add background color
+editor.addStyles({ backgroundColor: "yellow" });
+```
+
+#### Removing Styles
+
+```typescript
+removeStyles(styles: Styles): void
+```
+
+Removes specific styles from the currently selected text.
+
+```typescript
+// Remove single style
+editor.removeStyles({ bold: true });
+
+// Remove multiple styles
+editor.removeStyles({ bold: true, italic: true });
+
+// Remove color styles
+editor.removeStyles({ textColor: "red", backgroundColor: "yellow" });
+```
+
+#### Toggling Styles
+
+```typescript
+toggleStyles(styles: Styles): void
+```
+
+Toggles styles on the currently selected text (adds if not present, removes if present).
+
+```typescript
+// Toggle single style
+editor.toggleStyles({ bold: true });
+
+// Toggle multiple styles
+editor.toggleStyles({ bold: true, italic: true });
+
+// Toggle color
+editor.toggleStyles({ textColor: "blue" });
+```
+
+### Working with Links
+
+#### Creating Links
+
+```typescript
+createLink(url: string, text?: string): void
+```
+
+Creates a new link, optionally replacing the currently selected text.
+
+```typescript
+// Create link from selected text
+editor.createLink("https://blocknotejs.org");
+
+// Create link with custom text
+editor.createLink("https://blocknotejs.org", "Visit BlockNote");
+
+// Create link with empty URL (removes link)
+editor.createLink("");
+```
diff --git a/docs/content/docs/reference/editor/meta.json b/docs/content/docs/reference/editor/meta.json
new file mode 100644
index 0000000000..a82cdf3ea4
--- /dev/null
+++ b/docs/content/docs/reference/editor/meta.json
@@ -0,0 +1,4 @@
+{
+ "title": "Editor",
+ "pages": ["overview", "manipulating-content", "cursor-selections", "yjs-utilities", "..."]
+}
diff --git a/docs/content/docs/reference/editor/overview.mdx b/docs/content/docs/reference/editor/overview.mdx
new file mode 100644
index 0000000000..086d91c754
--- /dev/null
+++ b/docs/content/docs/reference/editor/overview.mdx
@@ -0,0 +1,132 @@
+---
+title: Overview
+description: An overview of the BlockNote editor API.
+imageTitle: Editor API
+---
+
+# BlockNote API Overview
+
+The BlockNote editor API is a comprehensive set of functions and methods that allow you to interact with the editor and manipulate its content.
+
+## Editable
+
+The editor is editable by default, but you can make it read-only by setting the `isEditable` property to `false`.
+
+```ts
+editor.isEditable = false;
+```
+
+## Focus
+
+To focus the editor, you can use the `focus` method.
+
+```ts
+editor.focus();
+```
+
+Check if the editor has focus.
+
+```ts
+const isFocused = editor.isFocused();
+```
+
+## Undo/Redo
+
+To undo the last operation, you can use the `undo` method.
+
+```ts
+editor.undo();
+```
+
+To redo the last undone operation, you can use the `redo` method.
+
+```ts
+editor.redo();
+```
+
+## Events
+
+To read more about events, see the [Events](/docs/reference/editor/events) reference.
+
+## Document Manipulation
+
+To read more about block manipulation, see the [Block Manipulation](/docs/reference/editor/manipulating-content#block-manipulation) reference.
+To read more about inline content manipulation, see the [Inline Content Manipulation](/docs/reference/editor/manipulating-content#inline-content-manipulation) reference.
+
+### Transactions
+
+BlockNote supports transactions, which allow you to group multiple changes into a single operation. This is useful for a better user experience, since undo/redo of changes is much more natural.
+
+```ts
+// ✅ Good - This is a single undo/redo operation
+editor.transact(() => {
+ editor.insertBlocks([{ type: "paragraph", content: "Hello, world!" }], "abc");
+ editor.replaceBlocks([{ id: "123" }], {
+ type: "paragraph",
+ content: "Hello, world!",
+ });
+});
+
+// ❌ Avoid - This is two separate undo/redo operations
+editor.insertBlocks([{ type: "paragraph", content: "Hello, world!" }], "abc");
+editor.replaceBlocks([{ id: "123" }], {
+ type: "paragraph",
+ content: "Hello, world!",
+});
+```
+
+## Cursor & Selections
+
+To read more about cursor and selection manipulation, see the [Cursor & Selections](/docs/reference/editor/cursor-selections) reference.
+
+## Paste Operations
+
+### Paste HTML
+
+Paste HTML content into the editor.
+
+```ts
+// Paste and convert to BlockNote format (default)
+editor.pasteHTML("
Hello, world!
");
+
+// Paste as raw HTML
+editor.pasteHTML("
Hello, world!
", true);
+```
+
+### Paste Text
+
+Paste text content into the editor.
+
+```ts
+editor.pasteText("Hello, world!");
+```
+
+### Paste Markdown
+
+Paste Markdown content into the editor.
+
+```ts
+editor.pasteMarkdown("# Hello\n\nThis is **bold** text.");
+```
+
+## Options
+
+The editor can be configured with the following options when using `BlockNoteEditor.create`:
+
+
+
+## YJS Utilities
+
+BlockNote provides utilities for working with YJS collaborative documents. These utilities allow you to convert between BlockNote blocks and YJS documents programmatically.
+
+To read more about YJS utilities, see the [YJS Utilities](/docs/reference/editor/yjs-utilities) reference.
+
+## Related Documentation
+
+For more detailed information about specific areas:
+
+- [Manipulating Content](/docs/foundations/manipulating-content) - Reading and writing document Content
+- [Block Types](/docs/features/blocks) - Understanding different block types
diff --git a/docs/content/docs/reference/editor/paste-handling.mdx b/docs/content/docs/reference/editor/paste-handling.mdx
new file mode 100644
index 0000000000..4115afb2cf
--- /dev/null
+++ b/docs/content/docs/reference/editor/paste-handling.mdx
@@ -0,0 +1,75 @@
+---
+title: Paste Handling
+description: This section explains how to handle paste events in BlockNote.
+---
+
+# Paste Handling
+
+BlockNote, by default, attempts to paste content in the following order:
+
+- VS Code compatible content
+- Files
+- BlockNote HTML
+- Markdown
+- HTML
+- Plain text
+
+
+ In certain cases, BlockNote will attempt to detect markdown in the clipboard
+ and paste that into the editor as rich text.
+
+
+You can change the default paste behavior by providing a custom paste handler, which will give you full control over how pasted content is inserted into the editor.
+
+## `pasteHandler` option
+
+The `pasteHandler` option is a function that receives the following arguments:
+
+```ts
+type PasteHandler = (context: {
+ event: ClipboardEvent;
+ editor: BlockNoteEditor;
+ defaultPasteHandler: (context?: {
+ prioritizeMarkdownOverHTML?: boolean;
+ plainTextAsMarkdown?: boolean;
+ }) => boolean;
+}) => boolean;
+```
+
+- `event`: The paste event.
+- `editor`: The current editor instance.
+- `defaultPasteHandler`: The default paste handler. If you only need to customize the paste behavior a little bit, you can fall back on the default paste handler.
+
+The `defaultPasteHandler` function can be called with the following options:
+
+- `prioritizeMarkdownOverHTML`: Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
+- `plainTextAsMarkdown`: Whether to interpret plain text as markdown and paste that as rich text or to paste the text directly into the editor.
+
+## Custom Paste Handler
+
+You can also provide your own paste handler by providing a function to the `pasteHandler` option.
+
+In this example, we handle the paste event if the clipboard data contains `text/my-custom-format`. If we don't handle the paste event, we call the default paste handler to do the default behavior.
+
+```ts
+const editor = new BlockNoteEditor({
+ pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+ if (event.clipboardData?.types.includes("text/my-custom-format")) {
+ // You can do any custom logic here, for example you could transform the clipboard data before pasting it
+ const markdown = customToMarkdown(
+ event.clipboardData.getData("text/my-custom-format"),
+ );
+
+ // The editor is able paste markdown (`pasteMarkdown`), HTML (`pasteHTML`), or plain text (`pasteText`)
+ editor.pasteMarkdown(markdown);
+ // We handled the paste event, so return true, returning false will cancel the paste event
+ return true;
+ }
+
+ // If we didn't handle the paste event, call the default paste handler to do the default behavior
+ return defaultPasteHandler();
+ },
+});
+```
+
+See an example of this in the [Custom Paste Handler](/examples/basic/custom-paste-handler) example.
diff --git a/docs/content/docs/reference/editor/yjs-utilities.mdx b/docs/content/docs/reference/editor/yjs-utilities.mdx
new file mode 100644
index 0000000000..1b2f7cba30
--- /dev/null
+++ b/docs/content/docs/reference/editor/yjs-utilities.mdx
@@ -0,0 +1,258 @@
+---
+title: YJS Utilities
+description: Utilities for converting between BlockNote blocks and YJS collaborative documents
+---
+
+# YJS Utilities
+
+The `@blocknote/core/yjs` export provides utilities for converting between BlockNote blocks and YJS collaborative documents. These utilities are useful when you need to work with YJS documents outside of the standard collaboration setup, such as importing existing content or working with YJS documents programmatically.
+
+
+ **Important:** This package is for advanced use cases where you need to
+ convert between BlockNote blocks and YJS documents programmatically. For most
+ use cases, you should use the [collaboration
+ features](/docs/features/collaboration) directly instead.
+
+
+## Import
+
+```typescript
+import {
+ blocksToYDoc,
+ blocksToYXmlFragment,
+ yDocToBlocks,
+ yXmlFragmentToBlocks,
+} from "@blocknote/core/yjs";
+```
+
+## Overview
+
+YJS utilities enable bidirectional conversion between:
+
+- **BlockNote blocks** ↔ **Y.Doc** (YJS document)
+- **BlockNote blocks** ↔ **Y.XmlFragment** (YJS XML fragment)
+
+These conversions are essential for:
+
+- Importing existing BlockNote content into a YJS document for collaboration
+- Exporting content from a YJS document back to BlockNote blocks
+- Working with YJS documents programmatically without an active editor instance
+
+## Converting Blocks to YJS Documents
+
+### `blocksToYDoc`
+
+Converts BlockNote blocks into a Y.Doc. This is useful when importing existing content to a Y.Doc for the first time.
+
+
+ **Important:** This should not be used to rehydrate a Y.Doc from a database
+ once collaboration has begun, as all history will be lost.
+
+
+```typescript
+function blocksToYDoc<
+ BSchema extends BlockSchema,
+ ISchema extends InlineContentSchema,
+ SSchema extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
+ xmlFragment?: string,
+): Y.Doc;
+```
+
+**Parameters:**
+
+- `editor` - The BlockNote editor instance
+- `blocks` - Array of blocks to convert
+- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`)
+
+**Returns:** A new Y.Doc containing the converted blocks
+
+**Example:**
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+import { blocksToYDoc } from "@blocknote/core/yjs";
+import * as Y from "yjs";
+
+const editor = BlockNoteEditor.create();
+
+const blocks = [
+ {
+ type: "paragraph",
+ content: "Hello, world!",
+ },
+ {
+ type: "heading",
+ props: { level: 1 },
+ content: "My Document",
+ },
+];
+
+// Convert blocks to Y.Doc
+const ydoc = blocksToYDoc(editor, blocks);
+
+// Now you can use this Y.Doc with a YJS provider for collaboration
+const provider = new WebrtcProvider("my-room", ydoc);
+```
+
+### `blocksToYXmlFragment`
+
+Converts BlockNote blocks into a Y.XmlFragment. This is useful when you want to work with a specific XML fragment within a Y.Doc.
+
+```typescript
+function blocksToYXmlFragment<
+ BSchema extends BlockSchema,
+ ISchema extends InlineContentSchema,
+ SSchema extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blocks: Block[],
+ xmlFragment?: Y.XmlFragment,
+): Y.XmlFragment;
+```
+
+**Parameters:**
+
+- `editor` - The BlockNote editor instance
+- `blocks` - Array of blocks to convert
+- `xmlFragment` - Optional existing Y.XmlFragment to populate (creates new one if not provided)
+
+**Returns:** A Y.XmlFragment containing the converted blocks
+
+**Example:**
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+import { blocksToYXmlFragment } from "@blocknote/core/yjs";
+import * as Y from "yjs";
+
+const editor = BlockNoteEditor.create();
+const doc = new Y.Doc();
+const fragment = doc.getXmlFragment("my-fragment");
+
+const blocks = [
+ {
+ type: "paragraph",
+ content: "Content for fragment",
+ },
+];
+
+// Convert blocks to the XML fragment
+blocksToYXmlFragment(editor, blocks, fragment);
+```
+
+## Converting YJS Documents to Blocks
+
+### `yDocToBlocks`
+
+Converts a Y.Doc back into BlockNote blocks. This is useful for reading content from a YJS document.
+
+```typescript
+function yDocToBlocks<
+ BSchema extends BlockSchema,
+ ISchema extends InlineContentSchema,
+ SSchema extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ ydoc: Y.Doc,
+ xmlFragment?: string,
+): Block[];
+```
+
+**Parameters:**
+
+- `editor` - The BlockNote editor instance
+- `ydoc` - The Y.Doc to convert
+- `xmlFragment` - Optional XML fragment name (defaults to `"prosemirror"`)
+
+**Returns:** Array of BlockNote blocks
+
+**Example:**
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+import { yDocToBlocks } from "@blocknote/core/yjs";
+import * as Y from "yjs";
+
+const editor = BlockNoteEditor.create();
+const ydoc = new Y.Doc();
+
+// ... Y.Doc is populated through collaboration or other means ...
+
+// Convert Y.Doc back to blocks
+const blocks = yDocToBlocks(editor, ydoc);
+
+console.log(blocks); // Array of BlockNote blocks
+```
+
+### `yXmlFragmentToBlocks`
+
+Converts a Y.XmlFragment back into BlockNote blocks.
+
+```typescript
+function yXmlFragmentToBlocks<
+ BSchema extends BlockSchema,
+ ISchema extends InlineContentSchema,
+ SSchema extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ xmlFragment: Y.XmlFragment,
+): Block[];
+```
+
+**Parameters:**
+
+- `editor` - The BlockNote editor instance
+- `xmlFragment` - The Y.XmlFragment to convert
+
+**Returns:** Array of BlockNote blocks
+
+**Example:**
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+import { yXmlFragmentToBlocks } from "@blocknote/core/yjs";
+import * as Y from "yjs";
+
+const editor = BlockNoteEditor.create();
+const doc = new Y.Doc();
+const fragment = doc.getXmlFragment("my-fragment");
+
+// ... Fragment is populated through collaboration or other means ...
+
+// Convert fragment back to blocks
+const blocks = yXmlFragmentToBlocks(editor, fragment);
+```
+
+## Round-trip Conversion
+
+All conversion functions support round-trip conversion, meaning you can convert blocks → YJS → blocks and get back the same content:
+
+```typescript
+import { BlockNoteEditor } from "@blocknote/core";
+import { blocksToYDoc, yDocToBlocks } from "@blocknote/core/yjs";
+
+const editor = BlockNoteEditor.create();
+
+const originalBlocks = [
+ {
+ type: "paragraph",
+ content: "Test content",
+ },
+];
+
+// Convert to Y.Doc and back
+const ydoc = blocksToYDoc(editor, originalBlocks);
+const convertedBlocks = yDocToBlocks(editor, ydoc);
+
+// originalBlocks and convertedBlocks are equivalent
+console.log(originalBlocks); // Same structure as convertedBlocks
+```
+
+## Related Documentation
+
+- [Real-time Collaboration](/docs/features/collaboration) - Learn how to set up collaboration in BlockNote
+- [Manipulating Content](/docs/reference/editor/manipulating-content) - Working with blocks and inline content
+- [Server Processing](/docs/features/server-processing) - Server-side processing for BlockNote (uses these YJS utilities internally)
diff --git a/docs/content/examples/index.mdx b/docs/content/examples/index.mdx
new file mode 100644
index 0000000000..2792a08136
--- /dev/null
+++ b/docs/content/examples/index.mdx
@@ -0,0 +1,8 @@
+---
+title: Examples
+description: Explore BlockNote examples
+---
+
+Browse through the examples below to see how to use and customize BlockNote. Want to contribute? Copy the [basic example on StackBlitz](https://stackblitz.com/github/TypeCellOS/BlockNote/tree/main/examples/01-basic/01-minimal/) and submit a PR.
+
+
diff --git a/docs/content/examples/meta.json b/docs/content/examples/meta.json
new file mode 100644
index 0000000000..303ef969fc
--- /dev/null
+++ b/docs/content/examples/meta.json
@@ -0,0 +1,18 @@
+{
+ "title": "Examples",
+ "description": "BlockNote examples",
+ "icon": "Building2",
+ "root": true,
+ "pages": [
+ "index",
+ "basic",
+ "backend",
+ "ui-components",
+ "theming",
+ "interoperability",
+ "custom-schema",
+ "collaboration",
+ "extensions",
+ "ai"
+ ]
+}
diff --git a/docs/content/pages/about.mdx b/docs/content/pages/about.mdx
new file mode 100644
index 0000000000..a3731ec631
--- /dev/null
+++ b/docs/content/pages/about.mdx
@@ -0,0 +1,56 @@
+---
+title: About
+description: BlockNote is an open source project led by Matthew Lipski, Nick Perez, and Yousef El-Dardiry. It's made possible by our partners, sponsors, and community members.
+---
+
+# About BlockNote
+
+BlockNote is an open source, modular rich-text editor designed for developers building modern web applications. With a focus on an easy developer experience and a modern, block-based user experience, BlockNote powers the text editing experience behind knowledge bases, note-taking apps, document editors, and internal tools.
+
+Our goal is to make it easy for developers to add a next-generation text editing experience to their app, with a UX that's on-par with industry leaders like Notion, Google Docs or Coda.
+
+BlockNote is developed by a team of engineers — led by Matthew Lipski, Nick Perez, and Yousef El-Dardiry — with deep expertise in rich text editing and real-time collaborative software. The project is made possible through the support of our partners, sponsors and open-source community.
+
+## Partner with us
+
+BlockNote is used by startups, public institutions and established companies to power their document editing experiences.
+
+We work closely with teams to integrate BlockNote into their products and build tailored solutions.
+
+Looking for hands-on help or a custom integration? Reach out at team@blocknotejs.org.
+
+
+
+
Get in touch
+
+
+
+## Support the project
+
+We're grateful to the individuals and organizations who support BlockNote. If you use BlockNote, the simplest way to contribute is by subscribing to BlockNote Pro.
+
+A subscription gives you access to Pro Examples, priority support from our team, and a commercial license for the XL packages. Learn more on the [pricing page](/pricing).
+
+We're proud to be supported by organizations like the renowned [NLNet foundation](https://www.nlnet.nl), which promotes a more open, privacy friendly, and secure internet.
+
+BlockNote is built on top of [ProseMirror](https://prosemirror.net/) and [Yjs](https://yjs.dev/) - two outstanding open source projects worth supporting.
+
+## Contribute
+
+Another great way to contribute is by becoming a Community member on Discord, help answering community questions and contribute to the codebase and documentation on GitHub.
+The community also greatly benefits from [examples](/examples), so don't hesitate to share useful code snippets!
+
+Check out our full list of contributors [on GitHub](https://github.com/TypeCellOS/BlockNote/graphs/contributors).
+
+
+
+ Star
+
+
diff --git a/docs/content/pages/legal/blocknote-xl-commercial-license.mdx b/docs/content/pages/legal/blocknote-xl-commercial-license.mdx
new file mode 100644
index 0000000000..26a7b72faa
--- /dev/null
+++ b/docs/content/pages/legal/blocknote-xl-commercial-license.mdx
@@ -0,0 +1,339 @@
+---
+title: BlockNote XL Commercial License
+description: BlockNote XL Commercial License
+---
+
+# BlockNote XL Commercial License
+
+## Important – Read Carefully
+
+This Commercial License constitutes a legally binding agreement (**Agreement**) between you or the business and/or entity which you represent (**Licensee**) and BlockNote (**Licensor**) for all BlockNote XL products included in this distribution/installation and associated documentation (**Software**).
+
+By purchasing, installing, copying, or otherwise using the Software, you acknowledge that you have read this Agreement and you agree to be bound by its terms and conditions. If you are representing a business and/or entity, you acknowledge that you have the legal authority to bind the business and/or entity you are representing to all the terms and conditions of this Agreement.
+
+If you do not agree to any of the terms and conditions of this Agreement or if you do not have the legal authority to bind the business and/or entity you are representing to any of the terms and conditions of this Agreement. Do not install, copy, use, evaluate, or replicate in any manner, any part, file or portion of the software development product(s).
+
+## 1.Definitions
+
+- "Affiliates" means the subsidiaries and the affiliates of the Licensee.
+- "Agreement" means this legally binding agreement that grants the Commercial License for all BlockNote XL products.
+- “Application” means either one (1) unique domain for web-based applications (excluding development, testing, or staging domains), or one (1) executable application instance for desktop or mobile use;
+- "GPL" means the GNU General Public License version 3.0.
+- "BlockNote" means OpenBlocks B.V., a legal entity registered under the Dutch Chamber of Commerce identifier 96110295.
+- "Commercial License" means the commercial license that is granted in this Agreement.
+- "Effective Date" means the date on which the Agreement is made effective. This is the date of the purchase.
+- "Front-end code" means the code that is executed in a browser, JavaScript most of the time.
+- "Licensee" means the business and/or entity that you represent.
+- "Licensor" means the creator and owner of the Software: BlockNote.
+- "License Term" means the duration for which the Commercial License is valid, starting from the Effective Date for an initial term of one month;
+- "Licensed Developers" means any person (employees, workers, and contractors) that are authorized by the Licensee to develop, modify, or integrate the software products that include the Software.
+- "OSS" means Open Source Software.
+- "Site" means all websites operated by BlockNote, including but not limited to https://www.blocknotejs.org.
+- "Software" means the copyrighted BlockNote XL Packages owned by Licensor, subject to the terms of this Agreement.
+- "Support Period" means the meaning that has been given to it in article 8.1.
+- "Permitted Third Party" means the meaning that has been given to it in article 2.8.
+- "Production environment" is where the User can see, experience, and interact with the product.
+- "User" means an end-user who accesses or interacts with the application or service provided by Licensee that incorporates the Software.
+- "XL Package" means specific components of the BlockNote library that are marked as XL and are subject to dual licensing. Including, but not limited to: Exporters (PDF / Docx / ODT), Multi-Column and Generative AI functionality.
+
+## 2. The license
+
+### 2.1. Choice of license
+
+Licensor offers the XL Packages under a dual license model. Licensor offers two licensing options:
+
+- a) the open source GPL, where any modifications or derivative works must also be made available under the license terms of GPL; and
+- b) this Commercial License, where you may use the Software under the terms and conditions mentioned in this Agreement.
+
+This Agreement is only applicable if you use the Commercial License.
+
+### 2.2. Evaluation (trial) license
+
+You are free to try the Software:
+
+- a) for a limited period of 30 days starting from the first use of the Software in a non-production environment; and
+- b) for the development of code not intended for production (for example, the reproduction of a bug in a GitHub issue, doing a performance benchmark).
+
+After the given trial period, you must license the Software if you continue to use it, whether in a production environment or non-production environment.
+
+### 2.3. Startup license
+
+Companies with fewer than five employees are eligible to use the Commercial License against a discount, subject to written confirmation. Please contact the Licensor if you believe you might be eligible for the discount.
+
+If, at any time during the License Term, the Licensee or its Affiliates employ five (5) or more individuals, the Licensee must promptly notify the Licensor and the Startup License shall automatically convert into a standard Commercial License at the full applicable license fee for the remainder of the License Term. The difference between the discounted fee and the full fee will become due upon conversion.
+
+Failure to report a change in entitlement of the startup license may result, at Licensor’s discretion, in termination of the Commercial License and this Agreement or in the invoicing of the discount granted for the period during which Licensee no longer met the entitlement conditions as mentioned in this article.
+
+### 2.4. Commercial License grant
+
+In exchange for the fee due under section 6 (Payment), or as otherwise agreed, Licensor hereby grants Licensee and Affiliates a Commercial License to install and use the Software.
+
+The Commercial License granted to Licensee and its Affiliates allows a worldwide, non-exclusive, non-transferable, sublicensable, royalty-free license, commencing on the Effective Date.
+
+The Commercial License entitles the Licensee to use the Software in development on a **single** Application and for deployment in a single Production Environment, subject to the License Term and other conditions of this Agreement. Continued use of the Software in any deployed Application requires an active Commercial License, even if no development activity occurs after deployment.
+
+The Commercial License allows the use of the latest version and all older versions released. Access to software updates is governed by section 7 (Updates).
+
+### 2.5. Usage rights
+
+Licensee may include the Software in a larger work containing more than the Software and may give limited usage rights (a sublicense) to Users as part of that larger work. The person or organization that receives this sublicense is the sublicensee. The sublicensee is allowed to use the Software only under the same conditions as the Licensee. However, the sublicensee shall not have the right to sublicense its rights.
+
+Licensee shall ensure (and shall procure that its Affiliates shall ensure) that the terms of any sublicense are in writing and are substantially the same and as restrictive as the terms of this Agreement.
+
+### 2.6. Restrictions
+
+Licensee shall not:
+
+- a) sell, rent, lease, distribute, assign, transfer, or encumber rights to the Software;
+- b) allow access to the Software by others not licensed under this Agreement;
+- c) share modified copies of the Software or documentation with others not licensed under this Agreement;
+- d) include any portion of the Software in any project that directly or indirectly competes with the Software. Directly competing means in any case developing a text editor framework or library for third party developers;
+- e) use the Commercial License for more than one Project as mentioned in section 2.4 (Commercial License grant)
+- f) use more than the permitted amount of seats as mentioned in section 2.7 (Required quantity of licenses); and
+- g) assist or allow others to use the Software against the terms of this Agreement.
+
+If the Licensor has any reasonable ground to believe that the Licensee violates this section 2.6 the Licensor may terminate the Commercial License directly and exclude the Licensee from any further use of the Software. In that case, the Licensor is not obliged to (re)pay any amounts already collected or any compensation.
+
+### 2.7. Required quantity of licenses for development
+
+A single Commercial License permits **five** Licensed Developers(seats) to use the Software in a single Application for development.
+
+The number of seats required corresponds to the maximum number of concurrent Licensed Developers (in any continuous 7-day period) contributing changes to the Front-end code of the projects that use the Software. Concurrent means development work carried out during the same calendar week, regardless of time zone.
+
+If the Licensee has not purchased a sufficient amount of Commercial Licenses, the Licensee must purchase the additional Commercial Licenses required to comply with this Agreement. The Licensor will determine the price, which may be up to the full original listed price at the Site.
+
+{/*
+
+### 2.8. Exemptions from Commercial License
+
+No Commercial License is required if:
+
+- a) you are an open source contributor who is not affiliated with the Licensee; or
+- b) you are someone who solely executes the front end for testing purposes, such as verifying back-end changes, and are not contributing changes to the Front-end code.
+
+Licensor is committed to supporting Open Source Software (OSS). When Licensee is developing OSS licensed under an Open Source Initiative approved license and not compatible with the GPL (e.g. a permissive license like MIT), Licensee may include the Software in its project and use the Commercial License free of charge, provided that:
+
+- a) the Software is used solely for the development or demonstration of your open source project;
+- b) Licensee informs its downstream users that enabling any feature relying on our Software will require compliance with either the GPL or the Commercial License.
+
+For clarity, inclusion of the Software in your OSS project, where it is completely disabled and not exposed to Users, does not trigger GPL reciprocity or the terms of this Commercial License until such functionality is enabled or made accessible.
+
+Please contact the Licensor for explicit permission or clarification if you intend to enable the Software in a publicly accessible or hosted environment, or if you are uncertain whether your use qualifies under these conditions.
+*/}
+
+### 2.8. Third party
+
+Licensee may allow its agents, contractors, and outsourcing service providers (each a **Permitted Third Party**) to use the Software licensed to Licensee hereunder solely for Licensee's benefit in accordance with the terms of this Agreement and Licensee is responsible for any such Permitted Third Party's compliance with this Agreement in such use. Any breach by any Permitted Third Party of the terms of this Agreement will be considered a breach by the Licensee.
+
+## 3. Term
+
+### 3.1. Monthly License
+
+The Licensor offers a Monthly license outside of production and in production.
+
+At the end of each License Term, the Commercial License will be automatically renewed always for the period of one month. Licensee can cancel the Commercial License before the end of a License Term by sending an email to team@blocknotejs.org. In case of timely cancellation the License Term will not be automatically renewed.
+
+### 3.2. Perpetual license
+
+Perpetual licenses are not offered as part of the standard license plans but may be available under custom licensing terms upon request.
+
+## 4. Source code
+
+4.1. Licensor shall make the Software available in source code form to Licensee. The source code is currently publicly available at https://github.com/typecellOS/blocknote.
+
+4.2. Licensee and its Affiliates shall be permitted to create modifications to the source code of the Software for Licensee's (and its Affiliates') use in accordance with this Agreement
+
+4.3. In the event that Licensee and/or its Affiliates create any modifications to the source code of the Software, Licensor shall not be obliged to provide support services in relation to any such modification(s). The warranty doesn't cover any such modification(s).
+
+## 5. Termination
+
+### 5.1. Termination for breach
+
+Either party may terminate this Agreement by giving the other written notice if:
+
+- a) the other party materially breaches any term of this Agreement and it is not possible to remedy that breach;
+- b) the other party materially breaches any term of this Agreement and it is possible to remedy that breach, but the other party fails to do so within 30 days of this being requested in writing;
+- c) the other party suffers or undergoes an insolvency event or a bankruptcy event or debtor's relief proceeding or ceases to do business.
+- d) the other party is delayed in performing its obligations under this Agreement for a period of 30 days or more.
+
+For the purposes of this clause, in order for it to be possible to remedy a breach, it must be possible to take steps so as to put the other party into the same position which (save as to the date) it would have been in if the breach had never occurred.
+
+### 5.2. Termination for convenience
+
+Licensee may terminate the Agreement, for its convenience, at any time in which event Licensee will not be entitled to a refund or credit of unused fees (if any) pre-paid by Licensee for access to the Software.
+
+### 5.3. Effect of termination
+
+Upon termination of this Agreement, Licensee's license to access and use the Software will terminate. Licensee must immediately cease all the use of the Software, including deployment in Production Environments, and destroy all copies of the Software in its possession (and require any permitted third parties to do the same), unless Licensee has obtained a separate license pursuant to written agreement with Licensor.
+
+### 5.4. Termination survival
+
+Any provisions of this Agreement containing licensing restrictions, warranties and warranty disclaimers, confidentiality obligations, limitations of liability and/or indemnity terms, and any term of this Agreement which, by its nature, is intended to survive termination or expiration, will remain in effect following any termination or expiration of this Agreement, as will Licensee's obligation to pay any fees accrued and owing to Licensor as of termination or expiration.
+
+### 5.5. Transition from Commercial License to GPL
+
+If the Licensee chooses to discontinue the Commercial License and instead rely solely on the GPL license, all rights and benefits granted under the Commercial License shall immediately terminate. This includes, but is not limited to, access to the Software, priority updates, and any form of technical support or assistance provided under the Commercial License.
+
+By transitioning to the GPL license, the Licensee agrees to comply fully with the terms of the GPL. The Licensor shall bear no further obligations toward the Licensee after the transition, and no refunds or credits shall be issued for any remaining term of the Commercial License.
+
+## 6. Payment
+
+### 6.1. Agreement
+
+Licensee agrees to pay the license fee for the Software specified on the Site or its quote at the time Licensee entered this Agreement.
+Licensor reserves the right to adjust the license fee at each renewal, subject to prior notice of 14 days.
+
+### 6.2. Terms
+
+The license fee is payable by Licensee on a monthly basis. Licensee must pay each valid invoice within 30 days of the invoice date, unless otherwise agreed. The payment must be made using an accepted payment method. If the payment is not made within the stipulated payment period, you will be automatically in default (verzuim).
+
+If the Licensee is in default, the Licensor may charge the Licensee the statutory commercial interest on all overdue payments. Licensee agrees to pay Licensor's cost of collecting any past-due amounts under this Agreement, including but not limited to reasonable attorneys' fees. Unless the currency is expressly provided, all amounts are in United States Dollars.
+
+### 6.3. Taxes
+
+Unless expressly provided, all amounts are exclusive of value-added tax which, where chargeable by Licensor, shall be payable by Licensee at the rate and in the manner prescribed by law. All other taxes, duties, customs, or similar charges shall be the responsibility of the Licensee.
+
+## 7. Updates
+
+### 7.1. Maintenance
+
+Licensee will be eligible to receive all updates and upgrades for the Software during the License Term at no additional charge, starting from the Effective Date.
+
+### 7.2. Renewal
+
+Licensee must renew the Commercial License for an additional License Term (and any subsequent term thereafter) in order to actively continue development with the Software, receive updates and upgrades, or maintain any use of the Software in Production Environments.
+
+### 7.3. Discontinuation
+
+Licensor reserves the right to discontinue the Software or any of its constituents, at any time by providing prior notice to Licensee.
+
+## 8. Support
+
+### 8.1. The support period
+
+For any applicable period for which you have purchased support (the **Support Period**), Licensee will be entitled to receive technical support for the Software. Unless otherwise specified, the Support Period starts from the Effective Date and is valid during the License Term. When the License Term ends, the support will end as well.
+
+### 8.2. Standard support
+
+This Agreement gives the Licensee entitlement to the standard support. This support plan is described in greater detail in the [Service Level Agreement](/legal/service-level-agreement) for technical support.
+
+### 8.3. Support renewal
+
+Support is renewed together with the renewal of the Commercial License and the addition of a License Term.
+
+## 9. Warranties
+
+### 9.1. Legal power
+
+Each party represents and warrants that it has the legal power and authority to enter into this Agreement.
+
+### 9.2. Intellectual property
+
+Licensor hereby represents and warrants that the Software does not and will not violate or infringe any third-party claims in regard to intellectual property, patents, trade secrets, and/or trademarks and that to the best of its knowledge no legal action has been taken against it for any infringement or violation of any third party intellectual property rights.
+
+### 9.3. Logic integrity
+
+Licensor warrants that the Software shall not knowingly include: malware, viruses, trap doors, back doors, or other means or functions which will detrimentally interfere with or otherwise adversely affect Licensee's use of the Software or which will damage or destroy data or other property of Licensee.
+
+### 9.4. Compliance with documentation
+
+Licensor warrants to Licensee that, for twelve (12) months after the Effective Date, the Software shall perform substantially in accordance with the documentation. Licensee's exclusive remedy, and Licensor's sole liability, with respect to any breach of this warranty, will be for Licensor to use commercially reasonable efforts to promptly correct the non-compliance (provided that Licensee notifies Licensor in writing within the warranty period and allow Licensor a reasonable cure period). If Licensor, at its discretion, reasonably determines that such correction is not economically or technically feasible, Licensor may revoke Licensee's Commercial License grant and provide Licensee with a full refund of the fee paid to Licensor.
+
+The Licensor provides no warranty, however, for unstable features of the Software. A feature is considered unstable if exposed to Licensee:
+
+- a) through an API that includes "unstable" in its name;
+- b) in a package for which the version is not considered stable according to SemVer versioning models, for example alpha, beta, or other pre-releases; or
+- c) documented as "experimental".
+
+### 9.5. Warranties disclaimers
+
+Except for the warranties expressly stated in the warranties section above, the Software is provided "as is", with all faults. Licensor disclaims all warranties, express or implied, including, but not limited to, warranties of merchantability, fitness for a particular purpose, title, availability, error-free or uninterrupted operation, and any warranties arising from course of dealing, course of performance, or usage of trade to the extent that licensor may not as a matter of applicable law disclaim any implied warranty, the scope, and duration of such warranty will be the minimum permitted under applicable law.
+
+## 10. Limitation of liability
+
+### 10.1. Exclusion of indirect and consequential damages
+
+To the maximum extent permitted by applicable law, in no event shall either party be liable for any special, incidental, indirect, or consequential damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or any other pecuniary loss) arising out of the use of or inability to use the Software or the provision of or failure to provide support, even if it has been advised of the possibility of such damages.
+
+### 10.2. Assumption of risk
+
+Licensee understands that the Software may produce inaccurate results because of a failure or fault within the Software or failure by Licensee to properly use and or deploy the Software. Except for Licensor's obligations regarding 9.2 Intellectual property, Licensee assumes full and sole responsibility for any use of the Software and bears the entire risk for failures or faults within the Software.
+
+### 10.3. Limitation of liability
+
+Each party agrees that regardless of the cause of failure or fault or the form of any claim. Each party’s sole remedy and sole obligation shall be governed by this Agreement and in no event shall either party’s liability exceed the price paid to the Licensor for the Software. This limited liability, as it relates to Licensor, is void if failure of the Software has resulted from accident, abuse, alteration, unauthorized use, or misapplication of the Software. The limitations and exclusions herein shall not apply to indemnification obligations hereunder.
+
+### 10.4 Lapse of claims
+
+The right to claim damages shall in any event lapse twelve (12) months after the event from which the damage directly or indirectly results and for which a party is liable.
+
+### 10.5 Gross negligence or willful misconduct
+
+Nothing in the Agreement shall limit or exclude a party’s liability to the extent it may not be excluded under any applicable laws, or it is the consequence of gross negligence or willful misconduct by a party.
+
+## 11. Indemnification
+
+### 11.1. Licensor's indemnification obligation
+
+Licensor will defend, indemnify and hold harmless Licensee from any claim of copyright, patent, trademark, trade secret, or other intellectual property right related to the Software developed by Licensor provided Licensee notifies Licensor in writing promptly upon notice of such claim and cooperates fully in the defense of such claim. Licensor shall, at its own expense, defend such claim, suit, or action, and Licensee shall have the right to participate in the defense at its own expense.
+
+### 11.2. Licensee's indemnification obligation
+
+Licensee hereby agrees to indemnify Licensor and its officers, directors, employees, agents, and representatives from each and every demand, claim, loss, liability, or damage of any kind, including actual attorney's fees, whether in tort or contract, that it or any of them may incur by reason of, or arising out of, any claim which is made by any third party with respect to any material breach or violation of this Agreement by Licensee. Licensor shall notify Licensee in writing promptly upon notice of such claim and cooperates fully in the defense of such claim.
+
+## 12. Force majeure
+
+Neither party will be liable for any delay or failure to take any action required under this Agreement (except for payment) due to any cause beyond the reasonable control of Licensee or Licensor, as the case may be, including, but not limited to: unavailability or shortages of labor, materials, or equipment, failure or delay in the delivery of vendors and suppliers, fire, flood, earthquake, acts of war, terrorism, epidemic, pandemic, and civil disorders.
+
+## 13. Personal data
+
+The privacy policy of the Licensor describes in detail how the Licensor as a controller processes personal data on its customers and community.
+
+All information Licensor collects from Licensee is stored and maintained on servers utilizing reasonable and appropriate data security safeguards. Licensor does not lend, lease, sell, or market information it obtains from its customers or those who provide Licensor personally identifiable information. Licensor does not disclose purchase information or licensing information to third parties.
+
+## 14. Confidentiality
+
+### 14.1. Confidentiality obligations and exceptions
+
+Each party shall:
+
+- a) maintain the confidentiality of all information received from the other party in connection with this Agreement;
+- b) use confidential information for the sole purpose of fulfilling the obligations under this Agreement unless otherwise agreed in writing between the parties;
+
+However, confidential information shall not include information that:
+
+- a) is generally known to the public at the time of disclosure;
+- b) is legally received by receiving party from a third party, which third party is in rightful possession of confidential information;
+- c) becomes generally known to the public subsequent to the time of such disclosure, but not as a result of disclosure by receiving party;
+- d) prior to signing of this Agreement, is already in the possession of receiving party; or
+- e) is independently developed by the receiving party without use of or reference to the confidential information of the disclosing party, as demonstrated by the receiving party's written records.
+
+### 14.2. Legally required disclosure
+
+Either party may disclose confidential information of the other party as required by governmental or judicial order, provided such party gives the other party prompt written notice prior to such disclosure (unless such prior notice is not permitted by applicable law) and complies with any protective order (or equivalent) imposed on such disclosure.
+
+## 15. Miscellaneous
+
+### 15.1 Notices
+
+Any notice or other communication required or permitted under the Agreement, shall be in English and sufficiently given through email to team@blocknotejs.org.
+
+### 15.2 Invalidity
+
+If a part of this Agreement is deemed void or voidable, this does not change the validity of the rest of this Agreement. Any invalid provision shall be replaced by a provision that is valid and which interpretation shall be as close as possible to the intent of the invalid provision.
+
+### 15.3 Enforcement
+
+No one other than a party to the Agreement, their successors and permitted assignees, will have any right to enforce any of its terms.
+
+### 15.4 Change of agreement
+
+The Licensor reserves the right to unilaterally amend or supplement this Agreement. If the Licensee continues to use the Commercial License after the effective date of the amended Agreement, the Licensee will be deemed to have accepted the amended terms.
+
+Any changes related to pricing will only apply to future updates of the Software. The Licensee shall retain access to the version of the Software available at the time of purchase under the pricing in effect at that time, unless otherwise agreed in writing.
+
+## 16. Choice of law and dispute resolution
+
+This Agreement is subject to Dutch law. If any dispute, controversy, or claim cannot be resolved by a good-faith discussion between the parties, then the Courts of the Netherlands (in first instance the Court of Rotterdam) shall have exclusive jurisdiction.
+
+---
diff --git a/docs/content/pages/legal/privacy-policy.mdx b/docs/content/pages/legal/privacy-policy.mdx
new file mode 100644
index 0000000000..22d3fa9669
--- /dev/null
+++ b/docs/content/pages/legal/privacy-policy.mdx
@@ -0,0 +1,132 @@
+---
+title: Privacy Policy
+description: We are BlockNote (OpenBlocks B.V.). We take your privacy seriously.
+---
+
+# Privacy Policy BlockNote
+
+This Privacy Policy was last updated on: 12 February 2025
+
+We are BlockNote (OpenBlocks B.V.). We take your privacy seriously. Sometimes, we need your Personal Data. We consider **Personal Data** to be any information relating to an identified or identifiable person, in conformity with the General Data Protection Regulation (the **GDPR**).
+
+This policy explains which Personal Data we use and why (the **Privacy Policy**). Furthermore, you will read how we process, store and protect your Personal Data. Finally, we outline what rights you have when we process your Personal Data.
+
+This Privacy Policy applies to our Website [https://www.blocknotejs.org](https://www.blocknotejs.org) (the **Website**), our online application (the **App**) and the services or products we provide (the **Services**). We process your Personal Data in accordance with the GDPR and all other relevant legislation and regulations in the field of protection of Personal Data, like Dutch Telecommunications Act (_Telecommunicatiewet_) regarding the use of cookies (the **Relevant Legislation**).
+
+## Are you under the age of 16?
+
+If you are younger than 16 years old, you need permission from your parents or legal guardian to use our Website, App and Services.
+
+## Processing of Personal Data
+
+In order to provide you with our Website, App and Services, we process your Personal Data.
+
+## How do we receive your Personal Data?
+
+**Personal data we receive from you:**
+
+We receive Personal Data directly from you when you log in via our website or interact with our applications.
+
+## Who is the controller of your Personal Data?
+
+We are the controller of your Personal Data within the meaning of the Relevant Legislation. At the end of this Policy, you can find our contact details .
+
+## What Personal Data do we process, for which specified purpose(s), and on which legal basis?
+
+We need some of your Personal Data in order for you to use our Website, Apps and Services.
+
+We are allowed to process your Personal Data, because we comply with the Relevant Legislation. We lawfully process your Personal Data because we:
+
+1. Have legal bases for processing your Personal Data;
+2. Inform you about the processing; and
+3. Only process data for specific purposes, and no more than is necessary for that.
+
+In the table below you will read (1) which Personal Data we process (2) for which purpose(s) and (3) on which legal basis.
+
+We shall only use your Personal Data for the following purposes or for compatible purposes. By doing so, we will not use your Personal Data in an unexpected manner.
+
+| **(Personal) Data** | **Purpose(s)** | **Legal Basis** |
+| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
+| _Contact Data:_ - Email address | _We use these Data:_ - For the delivery or performance of our Services to you | _We process these Data on the basis of:_ - Consent |
+| _Payment Data:_ - Payment Data of the paying party | _We use these Data:_ - To send invoices | _We process these Data on the basis of:_ - A necessity to perform the contract |
+| _Content Data related to the Services:_ - Correspondence or chat messages \- Your questions about our Services | _We use these Data:_ - To provide you with an optimal service | _We process these Data on the basis of:_ - A necessity to perform the contract |
+
+## Are you obliged to share your Personal Data with us?
+
+In some cases, the processing of your Personal Data is necessary. This is relevant, for example, when we have to process your Personal Data in order to oblige to a contract with you or to provide a service to you. Without your Personal Data, we cannot provide our Service to you.
+
+## How do we secure your Personal Data?
+
+We make every effort to protect your Personal Data from loss, destruction, use, alteration or dissemination of your Personal Data by unauthorized persons. We ensure that those who have nothing to do with your Personal Data cannot access it. We do this through the following measures:
+
+1. Secure network connections with Transport Layer Security (TLS), Secure Socket Layer (SSL), or a comparable technology
+2. The access to the Personal Data is strictly limited to the employees on a ‘need to know’ basis
+
+We constantly check our security measures for effectiveness, and if necessary adjust our process. That way, your Personal Data is always protected and accessible in the event of a failure.
+
+## How long do we store your Personal Data?
+
+We shall not store your Personal Data longer than the period in which we need them for the aforementioned purposes. We delete the Personal Data after we no longer need them for the purpose we process them for. The following is a list of the categories of Personal Data and the (functionally defined) retention periods:
+
+| **Category of Personal Data** | **Retention period** |
+| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
+| Contact Data | We retain your contact information for as long as necessary to provide our Services. |
+| Payment Data | We retain your payment data for as long as necessary to meet our financial and tax requirements and obligations. |
+| Partner and/or supplier Data | We retain partner or supplier data for as long as it is needed to provide our Services. |
+| Data for marketing and promotional reasons | We retain data for marketing and promotional purposes for as long as you wish to use these Services. |
+| Content data related to our Services | We retain content data for as long as necessary to provide you with our Services in an integral and continuous manner. |
+
+## With whom do we share your Personal Data?
+
+**Processors**
+
+We may share your Personal Data with data ‘processors’ within the meaning of the Relevant Legislation. We conclude a data processing agreement with these parties, which entails that they shall process your Personal Data carefully and that they shall only receive the Personal Data they need to provide their service. These parties shall only use your Personal Data in accordance with our instructions and not for their own purposes.
+
+If we have a legal obligation to share your Personal Data, we will do so. This is the case, for example, if a public authority legally requires us to share your Personal Data.
+
+**Third parties**
+
+Sometimes we share your Personal Data with other parties, who are not processors. With these parties, we agree that they shall use your Personal Data carefully. They shall only process your Personal Data for purposes compatible or in line with the purpose for which we received the Personal Data from you. For example, embed use third party integrations like Vercel for (anonymized) analytical purposes. Our example text editors can integrate with third party services such as TipTap, Y-Sweet, LiveBlocks and Partykit (CloudFlare).
+
+If you have any questions on the processing of your Personal Data of third party services, it is better to contact these services directly or consult their privacy policy.
+
+If we have a legal obligation to share your Personal Data, we will do so. This is the case, for example, if a public authority legally requires us to share your Personal Data.
+
+## Cookies
+
+A cookie is a small text file that can be sent via the server of a website to the browser. The browser saves this file to your computer. Your computer is tagged with a unique number, which enables our site to recognize that computer in the future.
+
+We use cookies to improve the user experience on our Website. Moreover, cookies ensure that the Website works faster, that you can visit our Website safely and that we can track and solve errors on our Website.
+
+You can always delete or disable cookies yourself via the browser settings. No more cookies will be stored when you visit our Website. However, please note that without cookies, our Website may not function as well as it should.
+
+## Other provisions
+
+We also process your Personal Data outside the European Economic Area (EEA). We only do so if a country provides an adequate level of protection for your Personal Data.
+
+**Websites of third parties**
+
+Our website and our App may contain links to other websites. We are not responsible for the content or the privacy protection on these websites. Therefore, we advise you to always read the privacy policy of those websites.
+
+## Your rights
+
+You have the following rights:
+
+1. **The right of access:** You can request access to your Personal Data;
+2. **The right to rectification:** You can request us to correct, limit or delete your Personal Data. In the event of fraud, non-payment or other wrongful acts, we can store some of your Personal Data in a register or on a blacklist;
+3. **The right to data portability:** You can request a copy of your Personal Data. We can provide this copy to third parties at your request, so you do not have to do so yourself;
+4. **The right to object:** You can object to the processing of your Personal Data;
+5. **The right to file a complaint:** You can file a complaint at the Dutch Data Protection Authority (Autoriteit Persoonsgegevens) if you are of the opinion that we wrongfully process your data;
+6. **The right to withdraw consent:** You can always withdraw your permission to process your Personal Data. From the moment of your withdrawal, we cannot process your Personal Data anymore.
+
+## Modifications to the Privacy Policy
+
+We may modify this Privacy Policy. If we substantially modify the Privacy Policy, we shall place a notification on our Website and in our App together with the new Privacy Policy. We shall notify registered users in case of a substantial modification. If you are not a registered user, we advise you to consult the Website and this Policy regularly.
+
+## Contact
+
+In the event that you wish to exercise these rights, or in the event of other questions or remarks regarding our Privacy Policy, you can contact us via the following contact details.
+
+BlockNote (OpenBlocks B.V.)
+
+team@blocknotejs.org
diff --git a/docs/content/pages/legal/service-level-agreement.mdx b/docs/content/pages/legal/service-level-agreement.mdx
new file mode 100644
index 0000000000..b733dbda1e
--- /dev/null
+++ b/docs/content/pages/legal/service-level-agreement.mdx
@@ -0,0 +1,108 @@
+---
+title: Service Level Agreement for Technical Support
+description: Service Level Agreement for Technical Support
+---
+
+# Service Level Agreement for Technical Support
+
+This Service Level Agreement ("SLA") is entered into by and between OpenBlocks B.V. ("BlockNote"), hereinafter also referred to as "Licensor," and Licensee, hereinafter referred to as "Licensee," effective as of the Effective Date.
+
+This SLA outlines the terms and conditions governing the provision of technical support services by the Licensor for the Software used by the Licensee. The objective of this SLA is to ensure that the Licensee receives timely and effective technical support for any issues arising in relation to the Software.
+
+This SLA defines the terms of the support included with, or purchased additionally to the BlockNote Commercial License.
+
+## 1. Definitions
+
+Same as in [BlockNote Commercial License](/legal/blocknote-xl-commercial-license).
+
+## 2. Support plans
+
+### 2.1. Standard Support
+
+BlockNote will provide the following services:
+
+- Prioritization of issues raised by the Licensee's team over those from community users
+- Direct access to the technical support team through GitHub and email
+
+### 2.2. Priority Support
+
+BlockNote will provide the following services:
+
+- 48-hour first-response time for reported issues
+- Prioritization of issues flagged by the Licensee's team over the "Standard Support" plan
+- Direct access to the technical support team through GitHub, email, and a dedicated Slack channel
+
+## 3. Support limitations
+
+### 3.1. Fair use
+
+While there is no hard limit on the number of support requests, BlockNote reserves the right to define and enforce a fair usage policy for the services provided under this SLA. The fair usage policy may include limits on the number of support requests, the scope of services, or other factors.
+
+Support does not include the development of significant new features. If your team needs assistance with building new functionality or extensive help with integrating BlockNote, our team is available for hire through the Enterprise Plan. Please [contact us](/about) for more details.
+
+### 3.2. General limitations
+
+BlockNote shall not be responsible for providing support for any issues arising from:
+
+- Modifications to the Software made by the Licensee without Licensor's written consent.
+- Use of the Software in conjunction with third-party software or hardware not supported by the Software.
+- Any failure or delay resulting from the Licensee's own network, hardware, or software.
+- Product training or guidance beyond an acceptable level, as determined by BlockNote.
+
+## 4. Service levels
+
+### 4.1. First-response time
+
+This first-response time begins when the Licensee completes the submission of a support ticket through BlockNote’s designated support channels. BlockNote guarantees the first-response time for all issues reported by the Licensee.
+
+### 4.2. Working hours
+
+BlockNote’s working hours are Monday through Friday, excluding weekends and holidays:
+
+- New Year's Day (January 1st)
+- Good Friday (date varies; Friday before Easter Sunday)
+- Easter Monday (date varies; Monday following Easter Sunday)
+- International Workers' Day (May 1st)
+- Christmas Day and Boxing day (December 25th and 26th)
+
+Please note that the observance of these holidays may vary by country, and additional holidays may be observed in specific regions or countries. If the Licensee submits a support ticket outside of BlockNote’s working hours, the First-response time will be calculated based on the remaining working hours and will resume at the start of the next working day.
+
+### 4.3. Identification and prioritization of bug fixes
+
+BlockNote will treat issues flagged by the Licensee as higher priority than issues for lower support plans. Upon bug identification, BlockNote will work with the Licensee to determine the following information:
+
+- a) A description of the bug;
+- b) An estimated timeline for the completion of the bug fix; and
+- c) Any known workarounds or temporary solutions available to the Licensee.
+
+Please note that the First-response does not necessarily include the information above, which will be determined separately as part of BlockNote’s collaboration with the Licensee.
+
+The estimated time for resolution is provided as a reference for planning purposes only, and is not a strict deadline. BlockNote will communicate to the Licensee any changes to the estimated timeline as soon as possible.
+
+### 4.4. Delivery of bug fixes and improvements
+
+All bug fixes are delivered in the latest version and are not backported.
+
+## 5. Support channels
+
+The Licensee may submit support requests and report bugs through the following channels:
+
+### 5.1. Email
+
+Submit support requests by emailing the team directly at team@blocknotejs.org.
+
+### 5.2. Slack
+
+Clients with a Priority Support plan can establish a direct line of communication with the BlockNote team via Slack Connect.
+
+### 5.3. GitHub
+
+Issues can be reported by opening a ticket at: https://github.com/TypeCellOS/BlockNote/issues/new/choose. For faster response times under your support plan, please share a link to the GitHub issue through email or Slack after submission.
+
+## 6. Review and amendments
+
+This SLA will be reviewed annually, or more frequently if required, to ensure that it continues to meet the needs of both parties. Any amendments to this SLA must be mutually agreed upon in writing by both BlockNote and the Licensee.
+
+## 7. Term and termination
+
+This SLA shall remain in effect for the duration of the License Term between BlockNote and the Licensee, unless terminated earlier by either party in accordance with the terms of their underlying agreement.
diff --git a/docs/content/pages/legal/terms-and-conditions.mdx b/docs/content/pages/legal/terms-and-conditions.mdx
new file mode 100644
index 0000000000..7b595aeb50
--- /dev/null
+++ b/docs/content/pages/legal/terms-and-conditions.mdx
@@ -0,0 +1,122 @@
+---
+title: Terms & Conditions
+description: These are our general terms and conditions.
+---
+
+# General terms and conditions
+
+## Who are we?
+
+We are BlockNote (OpenBlocks B.V.). We are registered with the Chamber of Commerce (_Kamer van Koophandel_) under number 96110295.
+
+## What do we do?
+
+We build open source software which you can use to build modern, collaborative text editors in your application (BlockNote).
+
+## What are you reading?
+
+These are our general terms and conditions (the **Terms**). You can also find them on [https://www.blocknotejs.org](https://www.blocknotejs.org) (the **Website**). In the **Privacy Policy** on our Website, we explain how we protect your personal data. Please take the time to read this carefully, as it includes important information about how we collect and use your data and why we do so.
+When we refer to _you_ in these Terms, we mean you as a user of our BlockNote.
+
+## Questions?
+
+If you have any questions regarding these Terms or BlockNote, do not hesitate to contact us by sending an email to team@blocknotejs.org.
+
+### Applicability of these Terms
+
+1. These Terms apply to every offer and agreement we make with you and any use of BlockNote.
+2. We reserve the right to change the Terms at all times. The latest version of the Terms will always apply.
+3. Arrangements that deviate from these Terms will only be applicable if they have been agreed on by us in writing (including email).
+
+### BlockNote
+
+1. BlockNote offers you access to the BlockNote open source library, examples and documentation which you can use as to power text editors in your application.
+2. We offer BlockNote to business users and consumers. **Consumer** means: any person not acting in the exercise of a profession or business.
+3. The open source software is available under the licenses specified in the source code (MPL 2.0 for the general library, and GPL 3.0 for the XL packages unless, specified otherwise).
+4. We offer additional services and licenses under separate terms. These are available as part through specific BlockNote Pro tiers, and include options like a [commercial license](/legal/blocknote-xl-commercial-license) for the XL packages and a [Service Level Agreement](/legal/service-level-agreement). If these specific terms conflict with our general Terms and Conditions, the commercial license terms will apply.
+
+### Subscriptions and cancellation of BlockNote Pro
+
+1. Before the use of BlockNote Pro you must enter into an agreement with us (**Subscription**). You can find more information about our Subscriptions on our Website.
+2. The term of the Subscription (**Subscription Period**) will be agreed during the application procedure. The Subscription will commence on the date as agreed during the application procedure (**Start Date**).
+3. If you have chosen a Subscription Period of 1 year, your Subscription will be automatically renewed, each time for the period of 12 months. If you have chosen a Subscription Period of 1 month, your Subscription will be renewed automatically, each time for the period of 1 month.
+4. You can cancel your Subscription before the end of the Subscription Period. You can cancel your subscription via the subscriber dashboard or by sending an email to team@blocknotejs.org.
+5. If you are a Consumer, you have the right to cancel your Subscription within 14 days of the Start Date. We will refund the payment to your means of payment if you have already made a payment for your Subscription.
+
+### Prices
+
+1. All prices communicated by us are _excluding_ any expenses and VAT.
+2. Listed prices and rates of our Subscriptions may change from time to time. The price change will take effect 1 month after its announcement.
+3. If you do not agree with the price change, you may cancel your Subscription within 1 month. The Subscription will then end on the date the price change takes effect.
+
+### Payment and collection charges
+
+1. You must provide us with a valid means of payment to use BlockNote Pro. We will debit the amount for the use of the Subscription on a monthly basis around the day on which your Subscription commenced.
+
+[//]: # "1. [KEEP, BUT COMMENT OUT] You must provide us with a valid means of payment to use BlockNote. We will debit the amount for the use of the Subscription on a monthly basis around the day on which your Subscription commenced."
+[//]: # "2. [KEEP, BUT COMMENT OUT] If a payment is due but not paid, we will first send a reminder, followed by a request for payment. If the payment is not made within the stipulated payment period, you will be in default. If you are in default, we are entitled to charge statutory commercial interest. If you are a Consumer, we may charge the statutory interest. The interest on the payable amount is calculated from the moment that the payment is due, until the moment that the payable amount has been paid by you in full."
+[//]: # "3. [KEEP, BUT COMMENT OUT] If you are in default, we have the right to (temporarily) block your account, until you fulfilled your payment obligation."
+[//]: # "4. [KEEP, BUT COMMENT OUT] If you are in default, you will also owe us all extrajudicial collection costs. For an outstanding amount up to EUR 267, these costs will be EUR 40. For a higher amount, the collection costs will be calculated on the basis of the outstanding amount."
+
+### Use of BlockNote Pro
+
+1. To access BlockNote Pro, you must make an account on our Website by signing in.
+2. You may only create a personal account for yourself. You may not give others access to (a backup copy of) BlockNote Pro.
+3. You need to keep your account details and password confidential. You are responsible for all activities on your account after it has been logged in with your account details and passwords, unless you have reported that your account has been compromised as soon as becoming aware of it.
+4. You can delete your account by emailing us. If we delete your account, this will in not constitute a termination or suspension of your payment obligations to us.
+
+[//]: # "2. [KEEP, BUT COMMENT OUT] You are responsible for choosing the correct means of identification, such as your e-mail address, and for choosing a strong password."
+
+### Availability and maintenance of BlockNote
+
+1. We will do our best to keep the BlockNote website up and running 24 hours a day, 7 days a week, during the time that you have a Subscription. We are responsible for the availability and maintenance of the BlockNote website.
+2. During maintenance BlockNote can be (partly) unavailable.
+3. We have the right to change BlockNote. This includes changing, removing or adding certain features or functionalities of BlockNote.
+4. We do not guarantee that BlockNote is completely free of errors.
+
+### Third Parties
+
+We have the right to employ third parties to partially perform our duties, if we are of the opinion that this is necessary for the due exercise of our Platform or Services. Sections 7:404 of the Dutch Civil Code (_DCC_) (_performance of service by a specific person_), 7:407 paragraph 2 DCC (_joint liability_) and 7:409 DCC (_death of a particularly assigned service provider_) are not applicable.
+
+### Force Majeure
+
+1. We will not be liable if we are unable to fulfil the Subscription with you due to force majeure. This includes, for example, a non-attributable failure of third parties that OpenBlocks B.V. uses, hacks and internet failures. This also applies if you cannot fulfil the Subscription due to force majeure.
+2. If the force majeure lasts longer than 3 months, you may terminate your Subscription in writing. In this case, there is no right to compensation. We will send you an invoice for the (unpaid) period in which you used BlockNote Pro.
+
+### Intellectual property
+
+1. We (or our licensors or suppliers) are the exclusive owners of all existing and future intellectual property, such as copyrights, trademarks, design rights, patents, source codes and know-how, which rest on BlockNote and our Website or are the fruits thereof.
+2. You only get the right to use BlockNote Pro. You cannot claim the intellectual property rights mentioned in paragraph 1.
+
+### Confidentiality and privacy
+
+1. We are obliged to keep all your Confidential Information confidential. By **Confidential Information** we mean any information that you have indicated is confidential or that arises from the nature of the information.
+2. In any case, the following is Confidential Information:
+ 1. Information relating to research and development, trade secrets or business information;
+ 2. Personal data as referred to in the General Data Protection Regulation (**GDPR**).
+3. We protect your personal data in accordance with the GDPR. Please see our Privacy Policy on the Website for more information.
+
+### Liability and indemnification
+
+1. We are not liable for any damage or other adverse consequences resulting from the use or inaccessibility of (information on) our Website or BlockNote. All actions you take on the basis of our Website or BlockNote are for your own account and risk.
+2. We will not be liable for any damages caused by improper or unlawful use of the BlockNote by you or third parties.
+3. We are not liable in the event of force majeure, as set out in the article entitled "Force Majeure".
+4. We are only liable for your direct damages, which are directly and exclusively the result of a shortcoming on our part.
+5. If we are nevertheless liable, our liability is always limited to a maximum of your annual Subscription price.
+6. We will ensure careful storage of your data. We are not liable for the damage or loss of data stored with us or third parties.
+7. The limitations of liability set out in this article do not apply if the damage is due to intent or gross negligence on our part.
+8. You shall indemnify and hold us harmless from any third party claims such as, but not limited to, fines, costs, damages, etc. relating to any use of BlockNote by you.
+
+### Miscellaneous
+
+1. The invalidity or un-enforceability of any provision of these Terms will not affect the validity or enforceability of any other provision of these Terms. Any such invalid or unenforceable provision will be replaced by a provision that is considered to be valid and enforceable and which' interpretation will be as close as possible to the intent of the invalid provision.
+2. You are not allowed to assign or transfer any rights and obligations on account of BlockNote or these Terms without prior written approval of us.
+
+### Applicable law
+
+Dutch law.
+
+### Competent court
+
+The Court of Rotterdam, the Netherlands has jurisdiction to hear all disputes or claims ensuing from these Terms. If you are a Consumer you may, within one month after BlockNote has invoked the jurisdiction of this court, choose the court that has jurisdiction under the law.
+You can also use the ODR (Online Dispute Resolution) platform if you are a Consumer. This platform offers a simple, effective, quick and inexpensive out-of-court solution to disputes arising from online transactions. For more information see: [http://ec.europa.eu/odr](http://ec.europa.eu/odr).
diff --git a/docs/content/pages/thanks.mdx b/docs/content/pages/thanks.mdx
new file mode 100644
index 0000000000..94a7a2ba9b
--- /dev/null
+++ b/docs/content/pages/thanks.mdx
@@ -0,0 +1,23 @@
+---
+title: Thank You for Subscribing!
+description: We're thrilled to have you as a BlockNote subscriber!
+---
+
+# Thank You for Subscribing!
+
+We're thrilled to have you as a BlockNote subscriber!
+
+
+
+Your support means the world to us and helps us continue developing and maintaining our open-source software.
+
+## What's Next?
+
+- **Access to Examples:** You now have full access to our comprehensive [examples](/examples) to help you get the most out of BlockNote.
+- **Priority GitHub Support:** As a subscriber, your GitHub issues will be prioritized, ensuring a faster resolution.
+- **Private Discord Channel:** Want to connect with us and other power users? Contact us via our [about page](/about) to get an invite to our private Discord channel.
+- **Showcase Your Support:** We'd love to show our gratitude for your sponsorship! If you'd like, you can send us your company logo by contacting us via our [about page](/about), and we'll feature it on our site.
+
+We truly appreciate your subscription and your commitment to open-source software.
+
+Welcome to the BlockNote community!
diff --git a/docs/emails/magic-link.tsx b/docs/emails/magic-link.tsx
new file mode 100644
index 0000000000..747a7d0df2
--- /dev/null
+++ b/docs/emails/magic-link.tsx
@@ -0,0 +1,97 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+
+export interface MagicLinkEmailProps {
+ name?: string;
+ url?: string;
+}
+
+export const MagicLinkEmail = ({ name, url }: MagicLinkEmailProps) => {
+ return (
+
+
+
+ BlockNote - Sign in to your account
+
+
+
+ Hi{name ? ` ${name}` : ""},
+
+ Someone recently requested a magic link for your BlockNote
+ account. If this was you, you can sign in here:
+
+
+
+ If you don't want to sign in or didn't request this,
+ just ignore and delete this message.
+
+
+ To keep your account secure, please don't forward this email
+ to anyone.
+
+
+
+
+
+ );
+};
+
+MagicLinkEmail.PreviewProps = {
+ name: "",
+ url: "https://blocknotejs.org",
+} as MagicLinkEmailProps;
+
+export default MagicLinkEmail;
+
+const main = {
+ backgroundColor: "#f6f9fc",
+ padding: "10px 0",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #f0f0f0",
+ padding: "45px",
+};
+
+const text = {
+ fontSize: "16px",
+ fontFamily:
+ "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
+ fontWeight: "300",
+ color: "#404040",
+ lineHeight: "26px",
+};
+
+const button = {
+ backgroundColor: "#007ee6",
+ borderRadius: "4px",
+ color: "#fff",
+ fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
+ fontSize: "15px",
+ textDecoration: "none",
+ textAlign: "center" as const,
+ display: "block",
+ width: "210px",
+ padding: "14px 7px",
+};
+
+const anchor = {
+ textDecoration: "underline",
+};
diff --git a/docs/emails/verify-email.tsx b/docs/emails/verify-email.tsx
new file mode 100644
index 0000000000..b6d8be7954
--- /dev/null
+++ b/docs/emails/verify-email.tsx
@@ -0,0 +1,94 @@
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+
+export interface VerifyEmailProps {
+ name?: string;
+ url?: string;
+}
+
+export const VerifyEmail = ({ name, url }: VerifyEmailProps) => {
+ return (
+
+
+
+ BlockNote - Verify your email address
+
+
+
+ Hi{name ? ` ${name}` : ""},
+
+ Thanks for signing up for BlockNote! To complete your
+ registration, please verify your email address by clicking the
+ button below:
+
+
+
+ If you didn't create a BlockNote account, you can safely
+ ignore this email.
+
+
+ To keep your account secure, please don't forward this email
+ to anyone.
+
+
+
+
+
+ );
+};
+
+VerifyEmail.PreviewProps = {
+ name: "",
+ url: "https://blocknotejs.org",
+} as VerifyEmailProps;
+
+export default VerifyEmail;
+
+const main = {
+ backgroundColor: "#f6f9fc",
+ padding: "10px 0",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #f0f0f0",
+ padding: "45px",
+};
+
+const text = {
+ fontSize: "16px",
+ fontFamily:
+ "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
+ fontWeight: "300",
+ color: "#404040",
+ lineHeight: "26px",
+};
+
+const button = {
+ backgroundColor: "#007ee6",
+ borderRadius: "4px",
+ color: "#fff",
+ fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
+ fontSize: "15px",
+ textDecoration: "none",
+ textAlign: "center" as const,
+ display: "block",
+ width: "210px",
+ padding: "14px 7px",
+};
diff --git a/docs/emails/welcome.tsx b/docs/emails/welcome.tsx
new file mode 100644
index 0000000000..1110f1dc3a
--- /dev/null
+++ b/docs/emails/welcome.tsx
@@ -0,0 +1,92 @@
+import {
+ Body,
+ Container,
+ Head,
+ Html,
+ Img,
+ Preview,
+ Section,
+ Text,
+ Link,
+} from "@react-email/components";
+
+export interface WelcomeEmailProps {
+ name?: string;
+}
+
+export const WelcomeEmail = ({ name }: WelcomeEmailProps) => {
+ return (
+
+
+
+ BlockNote - Next Steps & Subscription
+
+
+
+ Hi{name ? ` ${name}` : ""},
+
+ Thanks for verifying your email address and welcome to BlockNote!
+
+
+ Your next step is to subscribe to a BlockNote plan to unlock more
+ features. Subscribing to the Business plan grants you a license
+ for our XL packages. All paid plans also receive prioritized bug
+ support through GitHub.
+
+
+ View Plans & Subscribe
+
+
+ We appreciate your support for our open-source project!
+
+
+
+
+
+ );
+};
+
+WelcomeEmail.PreviewProps = {
+ name: "Alex",
+} as WelcomeEmailProps;
+
+export default WelcomeEmail;
+
+const main = {
+ backgroundColor: "#f6f9fc",
+ padding: "10px 0",
+};
+
+const container = {
+ backgroundColor: "#ffffff",
+ border: "1px solid #f0f0f0",
+ padding: "45px",
+};
+
+const text = {
+ fontSize: "16px",
+ fontFamily:
+ "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif",
+ fontWeight: "300",
+ color: "#404040",
+ lineHeight: "26px",
+};
+
+const button = {
+ backgroundColor: "#007ee6",
+ borderRadius: "4px",
+ color: "#fff",
+ fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
+ fontSize: "15px",
+ textDecoration: "none",
+ textAlign: "center" as const,
+ display: "block",
+ width: "230px",
+ padding: "14px 7px",
+ marginTop: "20px",
+};
diff --git a/docs/eslint.config.mjs b/docs/eslint.config.mjs
new file mode 100644
index 0000000000..bc72ced675
--- /dev/null
+++ b/docs/eslint.config.mjs
@@ -0,0 +1,17 @@
+import nextVitals from "eslint-config-next/core-web-vitals";
+import { defineConfig, globalIgnores } from "eslint/config";
+
+const eslintConfig = defineConfig([
+ ...nextVitals,
+ globalIgnores([
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ".source/**",
+ "components/fumadocs/**",
+ "components/example/generated/**", // TODO: fix lint of examples
+ ]),
+]);
+
+export default eslintConfig;
diff --git a/docs/instrumentation-client.ts b/docs/instrumentation-client.ts
new file mode 100644
index 0000000000..96a933a35e
--- /dev/null
+++ b/docs/instrumentation-client.ts
@@ -0,0 +1,34 @@
+// This file configures the initialization of Sentry on the client.
+// The added config here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: "https://31af815601e4174f4443c863953eebe7@o4508925169500160.ingest.de.sentry.io/4508925646078032",
+
+ // Add optional integrations for additional features
+ integrations: [Sentry.replayIntegration()],
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // Define how likely Replay events are sampled.
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // Define how likely Replay events are sampled when an error occurs.
+ replaysOnErrorSampleRate: 1.0,
+
+ // Enable sending user PII (Personally Identifiable Information)
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
+ sendDefaultPii: false,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/docs/instrumentation.ts b/docs/instrumentation.ts
new file mode 100644
index 0000000000..7cbe93c132
--- /dev/null
+++ b/docs/instrumentation.ts
@@ -0,0 +1,13 @@
+import * as Sentry from "@sentry/nextjs";
+
+export async function register() {
+ if (process.env.NEXT_RUNTIME === "nodejs") {
+ await import("./sentry.server.config");
+ }
+
+ if (process.env.NEXT_RUNTIME === "edge") {
+ await import("./sentry.edge.config");
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/docs/lib/auth-client.ts b/docs/lib/auth-client.ts
new file mode 100644
index 0000000000..ba5062cb3c
--- /dev/null
+++ b/docs/lib/auth-client.ts
@@ -0,0 +1,17 @@
+import type { auth } from "@/lib/auth";
+import { polarClient } from "@polar-sh/better-auth";
+import {
+ customSessionClient,
+ magicLinkClient,
+} from "better-auth/client/plugins";
+import { createAuthClient } from "better-auth/react";
+
+export const authClient = createAuthClient({
+ plugins: [
+ magicLinkClient(),
+ customSessionClient(),
+ polarClient(),
+ ],
+});
+
+export const { useSession, signIn, signOut, signUp } = authClient;
diff --git a/docs/lib/auth.ts b/docs/lib/auth.ts
new file mode 100644
index 0000000000..654ab8e9e9
--- /dev/null
+++ b/docs/lib/auth.ts
@@ -0,0 +1,298 @@
+import { checkout, polar, portal, webhooks } from "@polar-sh/better-auth";
+import { Polar } from "@polar-sh/sdk";
+import * as Sentry from "@sentry/nextjs";
+import { betterAuth } from "better-auth";
+import {
+ captcha,
+ createAuthMiddleware,
+ customSession,
+ magicLink,
+} from "better-auth/plugins";
+import { github } from "better-auth/social-providers";
+import Database from "better-sqlite3";
+import { Pool } from "pg";
+import { PRODUCTS } from "./product-list";
+import { sendEmail } from "./send-mail";
+
+export const polarClient = new Polar({
+ accessToken: process.env.POLAR_ACCESS_TOKEN,
+ // Use 'sandbox' if you're using the Polar Sandbox environment
+ // Remember that access tokens, products, etc. are completely separated between environments.
+ // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
+ server: process.env.NODE_ENV === "production" ? "production" : "sandbox",
+});
+
+export const auth = betterAuth({
+ user: {
+ additionalFields: {
+ planType: {
+ type: "string",
+ required: false,
+ input: false, // don't allow user to set plan type
+ },
+ ghSponsorInfo: {
+ type: "string",
+ required: false,
+ input: false, // don't allow user to set role
+ },
+ },
+ },
+ emailVerification: {
+ sendOnSignUp: true,
+ autoSignInAfterVerification: true,
+ async sendVerificationEmail({ user, url }) {
+ await sendEmail({
+ to: user.email,
+ template: "verifyEmail",
+ props: { url, name: user.name },
+ });
+ },
+ },
+ emailAndPassword: {
+ enabled: true,
+ requireEmailVerification: true,
+ autoSignIn: true,
+ },
+ socialProviders: {
+ github: {
+ clientId: process.env.AUTH_GITHUB_ID as string,
+ clientSecret: process.env.AUTH_GITHUB_SECRET as string,
+ async getUserInfo(token) {
+ // This is a workaround to still re-use the default github provider getUserInfo
+ // and still be able to fetch the sponsor info with the token
+ return (await github({
+ clientId: process.env.AUTH_GITHUB_ID as string,
+ clientSecret: process.env.AUTH_GITHUB_SECRET as string,
+ async mapProfileToUser() {
+ const resSponsor = await fetch(`https://api.github.com/graphql`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token.accessToken}`,
+ },
+ // organization(login:"TypeCellOS") {
+ // user(login:"YousefED") {
+ body: JSON.stringify({
+ query: `{
+ user(login:"YousefED") {
+ sponsorshipForViewerAsSponsor(activeOnly:false) {
+ isActive,
+ tier {
+ name
+ monthlyPriceInDollars
+ }
+ }
+ }
+ }`,
+ }),
+ });
+ if (resSponsor.ok) {
+ // Mock data. TODO: disable and test actial data
+ // profile.sponsorInfo = {
+ // isActive: true,
+ // tier: {
+ // name: "test",
+ // monthlyPriceInDollars: 100,
+ // },
+ // };
+ // use API data:
+ const data = await resSponsor.json();
+ //// eslint-disable-next-line no-console
+ console.log("sponsor data", data);
+ // {
+ // "data": {
+ // "user": {
+ // "sponsorshipForViewerAsSponsor": {
+ // "isActive": true,
+ // "tier": {
+ // "name": "$90 a month",
+ // "monthlyPriceInDollars": 90
+ // }
+ // }
+ // }
+ // }
+ // }
+ const sponsorInfo: null | {
+ isActive: boolean;
+ tier: {
+ monthlyPriceInDollars: number;
+ };
+ } = data.data.user.sponsorshipForViewerAsSponsor;
+ if (!sponsorInfo?.isActive) {
+ return {};
+ }
+ return {
+ ghSponsorInfo: JSON.stringify(sponsorInfo),
+ };
+ }
+ return {};
+ },
+ }).getUserInfo(token))!;
+ },
+ },
+ },
+ // Use SQLite for local development
+ database:
+ process.env.NODE_ENV === "production" || process.env.POSTGRES_URL
+ ? new Pool({
+ connectionString: process.env.POSTGRES_URL,
+ })
+ : new Database("./sqlite.db"),
+ plugins: [
+ captcha({
+ provider: "cloudflare-turnstile",
+ secretKey: process.env.TURNSTILE_SECRET_KEY!,
+ endpoints: ["/sign-up/email"],
+ }),
+ customSession(
+ async ({ user, session }) => {
+ // If they are a GitHub sponsor, use that plan type
+ if (user.ghSponsorInfo) {
+ const sponsorInfo = JSON.parse(user.ghSponsorInfo);
+ return {
+ planType:
+ sponsorInfo.tier.monthlyPriceInDollars > 100
+ ? "business"
+ : "starter",
+ user,
+ session,
+ };
+ }
+ // If not, see if they are subscribed to a Polar product
+ // If not, use the free plan
+ return {
+ planType: user.planType ?? PRODUCTS.free.slug,
+ user,
+ session,
+ };
+ },
+ {
+ // This is really only for type inference
+ user: {
+ additionalFields: {
+ ghSponsorInfo: {
+ type: "string",
+ required: false,
+ input: false, // don't allow user to set role
+ },
+ planType: {
+ type: "string",
+ required: false,
+ input: false, // don't allow user to set plan type
+ },
+ },
+ },
+ },
+ ),
+ magicLink({
+ sendMagicLink: async ({ email, url }) => {
+ await sendEmail({
+ to: email,
+ template: "magicLink",
+ props: { url },
+ });
+ },
+ }),
+ // Just temporary for testing
+ // Serves on http://localhost:3000/api/auth/reference
+ // openAPI(),
+ polar({
+ client: polarClient,
+ // Enable automatic Polar Customer creation on signup
+ createCustomerOnSignUp: true,
+ use: [
+ checkout({
+ products: [
+ {
+ productId: PRODUCTS.business.id, // ID of Product from Polar Dashboard
+ slug: PRODUCTS.business.slug, // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
+ },
+ {
+ productId: PRODUCTS["business-yearly"].id,
+ slug: PRODUCTS["business-yearly"].slug,
+ },
+ {
+ productId: PRODUCTS.starter.id,
+ slug: PRODUCTS.starter.slug,
+ },
+ ],
+ successUrl: "/thanks",
+ authenticatedUsersOnly: true,
+ }),
+ portal(),
+ webhooks({
+ secret: process.env.POLAR_WEBHOOK_SECRET as string,
+ async onPayload(payload) {
+ switch (payload.type) {
+ case "subscription.active":
+ case "subscription.canceled":
+ case "subscription.updated":
+ case "subscription.revoked":
+ case "subscription.created":
+ case "subscription.uncanceled": {
+ const authContext = await auth.$context;
+ const userId = payload.data.customer.externalId;
+ if (!userId) {
+ return;
+ }
+ if (payload.data.status === "active") {
+ const productId = payload.data.product.id;
+ const planType = Object.values(PRODUCTS).find(
+ (p) => p.id === productId,
+ )?.slug;
+ await authContext.internalAdapter.updateUser(userId, {
+ planType,
+ });
+ } else {
+ // No active subscription, so we need to remove the plan type
+ await authContext.internalAdapter.updateUser(userId, {
+ planType: null,
+ });
+ }
+ }
+ }
+ },
+ }),
+ ],
+ }),
+ ],
+ onAPIError: {
+ onError: (error) => {
+ Sentry.captureException(error, {
+ tags: { source: "better-auth" },
+ level: "fatal",
+ });
+ },
+ },
+ hooks: {
+ after: createAuthMiddleware(async (ctx) => {
+ if (
+ ctx.path === "/magic-link/verify" ||
+ ctx.path === "/verify-email" ||
+ ctx.path === "/sign-in/social"
+ ) {
+ // After verifying email, send them a welcome email
+ const newSession = ctx.context.newSession;
+ if (newSession) {
+ const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
+ if (
+ ctx.path === "/magic-link/verify" &&
+ newSession.user.createdAt < oneMinuteAgo
+ ) {
+ // magic link is for an account that was created more than a minute ago, so just a normal sign in
+ // no need to send welcome email
+ return false;
+ }
+ // await sendEmail({
+ // to: newSession.user.email,
+ // template: "welcome",
+ // props: {
+ // name: newSession.user.name,
+ // },
+ // });
+ return;
+ }
+ }
+ }),
+ },
+});
diff --git a/docs/lib/fumadocs/README.md b/docs/lib/fumadocs/README.md
new file mode 100644
index 0000000000..8830900a4d
--- /dev/null
+++ b/docs/lib/fumadocs/README.md
@@ -0,0 +1 @@
+files in this directory have been added by ejecting parts of fumadocs with `npx @fumadocs/cli customise`
diff --git a/docs/lib/fumadocs/cn.ts b/docs/lib/fumadocs/cn.ts
new file mode 100644
index 0000000000..ba66fd250b
--- /dev/null
+++ b/docs/lib/fumadocs/cn.ts
@@ -0,0 +1 @@
+export { twMerge as cn } from 'tailwind-merge';
diff --git a/docs/lib/fumadocs/merge-refs.ts b/docs/lib/fumadocs/merge-refs.ts
new file mode 100644
index 0000000000..cf019f13b0
--- /dev/null
+++ b/docs/lib/fumadocs/merge-refs.ts
@@ -0,0 +1,13 @@
+import type * as React from 'react';
+
+export function mergeRefs(...refs: (React.Ref | undefined)[]): React.RefCallback {
+ return (value) => {
+ refs.forEach((ref) => {
+ if (typeof ref === 'function') {
+ ref(value);
+ } else if (ref) {
+ ref.current = value;
+ }
+ });
+ };
+}
diff --git a/docs/lib/fumadocs/urls.ts b/docs/lib/fumadocs/urls.ts
new file mode 100644
index 0000000000..2cc3b4efd0
--- /dev/null
+++ b/docs/lib/fumadocs/urls.ts
@@ -0,0 +1,14 @@
+export function normalize(urlOrPath: string) {
+ if (urlOrPath.length > 1 && urlOrPath.endsWith('/')) return urlOrPath.slice(0, -1);
+ return urlOrPath;
+}
+
+/**
+ * @returns if `href` is matching the given pathname
+ */
+export function isActive(href: string, pathname: string, nested = true): boolean {
+ href = normalize(href);
+ pathname = normalize(pathname);
+
+ return href === pathname || (nested && pathname.startsWith(`${href}/`));
+}
diff --git a/docs/lib/getExampleData.ts b/docs/lib/getExampleData.ts
new file mode 100644
index 0000000000..1e0a2a5127
--- /dev/null
+++ b/docs/lib/getExampleData.ts
@@ -0,0 +1,21 @@
+import {
+ ExampleData,
+ exampleGroupsData,
+} from "@/components/example/generated/exampleGroupsData.gen";
+
+export function getExampleData(
+ exampleGroupName: string,
+ exampleName: string,
+): ExampleData {
+ const exampleData = exampleGroupsData
+ .find((exampleGroup) => exampleGroup.exampleGroupName === exampleGroupName)
+ ?.examplesData.find((example) => example.exampleName === exampleName);
+
+ if (!exampleData) {
+ throw new Error(
+ `Example ${exampleGroupName}/${exampleName} could not be found in exampleGroupsData.gen.ts. Either an invalid example/group name was provided, or something went wrong when generating the exampleGroupsData.gen.ts file.`,
+ );
+ }
+
+ return exampleData;
+}
diff --git a/docs/lib/getFullMetadata.ts b/docs/lib/getFullMetadata.ts
new file mode 100644
index 0000000000..d0f5653dce
--- /dev/null
+++ b/docs/lib/getFullMetadata.ts
@@ -0,0 +1,37 @@
+import { Metadata } from "next";
+
+export const getFullMetadata = (metadata: {
+ title: string;
+ description?: string;
+ path?: string;
+ openGraphImages?: Exclude["images"];
+}): Metadata => ({
+ metadataBase: "https://www.blocknotejs.org",
+ title: `BlockNote - ${metadata.title}`,
+ description: metadata.description,
+ icons: {
+ icon: [
+ { url: "/favicon.ico", sizes: "any", type: "image/x-icon" },
+ { url: "/favicon.svg", sizes: "any", type: "image/svg+xml" },
+ { url: "/favicon.png", type: "image/png" },
+ ],
+ apple: { url: "/apple-touch-icon.png", type: "image/png" },
+ },
+ manifest: "/site.webmanifest",
+ openGraph: {
+ images: metadata.openGraphImages || "/og/image.png",
+ locale: "en_US",
+ siteName: "BlockNote",
+ type: "website",
+ url: metadata.path || "/",
+ },
+ robots: {
+ follow: true,
+ index: true,
+ },
+ twitter: {
+ card: "summary_large_image",
+ creator: "@TypeCellOS",
+ site: "@TypeCellOS",
+ },
+});
diff --git a/docs/lib/layout.shared.tsx b/docs/lib/layout.shared.tsx
new file mode 100644
index 0000000000..3fdf6e2329
--- /dev/null
+++ b/docs/lib/layout.shared.tsx
@@ -0,0 +1,394 @@
+import { AuthNavButton } from "@/components/AuthNavButton";
+import ThemedImage from "@/components/ThemedImage";
+import LogoDark from "@/public/img/logos/banner.dark.svg";
+import LogoLight from "@/public/img/logos/banner.svg";
+import type { BaseLayoutProps, LinkItemType } from "fumadocs-ui/layouts/shared";
+import { FaGithub } from "react-icons/fa6";
+// import { LinkItemType } from "@/components/fumadocs/layout/link-item";
+// import {
+// ActivityIcon,
+// BotIcon,
+// BoxIcon,
+// CpuIcon,
+// GlobeIcon,
+// LockIcon,
+// RocketIcon,
+// ShieldCheckIcon,
+// SparklesIcon,
+// } from "lucide-react";
+
+// const links: LinkItemType[] = [
+// {
+// text: "Docs",
+// url: "/docs",
+// active: "nested-url",
+// items: [
+// {
+// text: "Overview",
+// url: "/docs/overview",
+// },
+// ],
+// },
+
+// {
+// type: "menu",
+// text: "Product",
+// items: [
+// {
+// groupName: "Platform",
+// text: "Editor Platform",
+// description: "Modern block-based editor",
+// url: "/product/platform/editor",
+// icon: ,
+// },
+// {
+// groupName: "Platform",
+// text: "Block Model",
+// description: "Composable, structured content",
+// url: "/product/platform/block-model",
+// icon: ,
+// },
+// {
+// groupName: "Platform",
+// text: "Real-time Collaboration",
+// description: "Multi-user editing with presence",
+// url: "/product/platform/collaboration",
+// icon: ,
+// },
+// {
+// groupName: "Platform",
+// text: "Extensibility",
+// description: "Custom blocks, plugins, and APIs",
+// url: "/product/platform/extensibility",
+// icon: ,
+// },
+// {
+// groupName: "Platform",
+// text: "Performance & Scalability",
+// description: "Reliable at any scale",
+// url: "/product/platform/performance",
+// icon: ,
+// },
+// {
+// groupName: "Features",
+// text: "Custom Blocks",
+// description: "Extend functionality with your own blocks",
+// url: "/product/features/custom-blocks",
+// icon: ,
+// },
+// {
+// groupName: "Features",
+// text: "Collaborative Editing",
+// description: "Edit documents with your team in real time",
+// url: "/product/features/collaboration",
+// icon: ,
+// },
+// {
+// groupName: "Features",
+// text: "Comments & Mentions",
+// description: "Streamline team communication in context",
+// url: "/product/features/comments",
+// icon: ,
+// },
+// {
+// groupName: "Features",
+// text: "Version History",
+// description: "Track changes and revert safely",
+// url: "/product/features/version-history",
+// icon: ,
+// },
+// {
+// groupName: "Integrations",
+// text: "Yjs Collaboration Engine",
+// description: "Reliable CRDT-based synchronization",
+// url: "/product/integrations/yjs",
+// icon: ,
+// },
+// {
+// groupName: "Integrations",
+// text: "ProseMirror Ecosystem",
+// description: "Interoperable editor architecture",
+// url: "/product/integrations/prosemirror",
+// icon: ,
+// },
+// {
+// groupName: "Integrations",
+// text: "Framework Integrations",
+// description: "React, Next.js, and more",
+// url: "/product/integrations/frameworks",
+// icon: ,
+// },
+// {
+// groupName: "Integrations",
+// text: "API & SDKs",
+// description: "Extend and automate workflows",
+// url: "/product/integrations/api-sdk",
+// icon: ,
+// },
+// ],
+// },
+
+// {
+// type: "menu",
+// text: "Solutions",
+// items: [
+// {
+// groupName: "Use Cases",
+// text: "Knowledge Bases",
+// description: "Centralize and structure knowledge",
+// url: "/solutions/use-cases/knowledge-bases",
+// icon: ,
+// },
+// {
+// groupName: "Use Cases",
+// text: "Collaborative Documents",
+// description: "Team-driven document workflows",
+// url: "/solutions/use-cases/collaborative-docs",
+// icon: ,
+// },
+// {
+// groupName: "Use Cases",
+// text: "Internal Tools",
+// description: "Custom apps built on BlockNote",
+// url: "/solutions/use-cases/internal-tools",
+// icon: ,
+// },
+// {
+// groupName: "Industries",
+// text: "Public Sector",
+// description: "Digital autonomy & EU collaborations",
+// url: "/solutions/industries/public-sector",
+// icon: ,
+// },
+// {
+// groupName: "Industries",
+// text: "Enterprise Teams",
+// description: "Custom collaboration & consulting",
+// url: "/solutions/industries/enterprise",
+// icon: ,
+// },
+// {
+// groupName: "Industries",
+// text: "Startups & Scaleups",
+// description: "Flexible partnerships for growth-stage companies",
+// url: "/solutions/industries/startups",
+// icon: ,
+// },
+// ],
+// },
+
+// {
+// type: "menu",
+// text: "AI",
+// items: [
+// {
+// groupName: "AI Features",
+// text: "AI Writing Assistance",
+// description: "Content generation powered by AI",
+// url: "/ai/features/writing-assistance",
+// icon: ,
+// },
+// {
+// groupName: "AI Features",
+// text: "Smart Blocks",
+// description: "Context-aware content blocks",
+// url: "/ai/features/smart-blocks",
+// icon: ,
+// },
+// {
+// groupName: "AI Use Cases",
+// text: "Knowledge Capture",
+// description: "Automate knowledge aggregation",
+// url: "/ai/use-cases/knowledge-capture",
+// icon: ,
+// },
+// {
+// groupName: "AI Use Cases",
+// text: "Content Drafting",
+// description: "Draft documents efficiently",
+// url: "/ai/use-cases/content-drafting",
+// icon: ,
+// },
+// {
+// groupName: "AI Platform",
+// text: "Model-Agnostic Design",
+// description: "Flexible AI integration",
+// url: "/ai/platform/model-agnostic",
+// icon: ,
+// },
+// {
+// groupName: "AI Platform",
+// text: "Data Privacy & Security",
+// description: "Enterprise-grade compliance",
+// url: "/ai/platform/privacy-security",
+// icon: ,
+// },
+// ],
+// },
+
+// {
+// type: "menu",
+// text: "Resources",
+// items: [
+// {
+// groupName: "Documentation & Developer Tools",
+// text: "Get Started",
+// description: "Learn how to use BlockNote",
+// url: "/resources/get-started",
+// icon: ,
+// },
+// {
+// groupName: "Documentation & Developer Tools",
+// text: "Installation",
+// description: "Set up BlockNote in your environment",
+// url: "/resources/installation",
+// icon: ,
+// },
+// {
+// groupName: "Documentation & Developer Tools",
+// text: "Guides",
+// description: "In-depth tutorials and examples",
+// url: "/resources/guides",
+// icon: ,
+// },
+// {
+// groupName: "Documentation & Developer Tools",
+// text: "API Reference",
+// description: "Technical documentation for developers",
+// url: "/resources/api",
+// icon: ,
+// },
+// {
+// groupName: "Learning & Proof",
+// text: "Case Studies",
+// description: "Real-world use cases of BlockNote",
+// url: "/resources/case-studies",
+// icon: ,
+// },
+// {
+// groupName: "Learning & Proof",
+// text: "Open-Source Deployments",
+// description: "Examples of OSS projects using BlockNote",
+// url: "/resources/oss-deployments",
+// icon: ,
+// },
+// {
+// groupName: "Learning & Proof",
+// text: "Comparisons",
+// description: "How BlockNote compares to other editors",
+// url: "/resources/comparisons",
+// icon: ,
+// },
+// {
+// groupName: "About & Trust",
+// text: "About BlockNote",
+// description: "Mission, vision, and OSS philosophy",
+// url: "/resources/about",
+// icon: ,
+// },
+// {
+// groupName: "About & Trust",
+// text: "Security & Privacy",
+// description: "Compliance and enterprise-grade trust",
+// url: "/resources/security-privacy",
+// icon: ,
+// },
+// {
+// groupName: "About & Trust",
+// text: "Digital Sovereignty",
+// description: "Collaborations with governments and EU initiatives",
+// url: "/resources/digital-sovereignty",
+// icon: ,
+// },
+// {
+// groupName: "About & Trust",
+// text: "Careers & Partners",
+// description: "Work with or join BlockNote",
+// url: "/resources/careers-partners",
+// icon: ,
+// },
+// {
+// groupName: "About & Trust",
+// text: "Enterprise Inquiries",
+// description: "Custom consultation and collaboration",
+// url: "/resources/enterprise",
+// icon: ,
+// },
+// ],
+// },
+// ];
+
+const links: LinkItemType[] = [
+ // {
+ // type: "menu",
+ // icon: ,
+ // text: "Profile",
+ // items: [
+ // {
+ // text: "Getting Started",
+ // description: "Learn to use Fumadocs",
+ // url: "/docs",
+ // },
+ // ],
+ // },
+ {
+ text: "Docs",
+ url: "/docs",
+ active: "nested-url",
+ },
+ {
+ text: "AI",
+ url: "/docs/features/ai",
+ active: "nested-url",
+ },
+ {
+ text: "Examples",
+ url: "/examples",
+ active: "nested-url",
+ },
+ {
+ text: "Pricing",
+ url: "/pricing",
+ active: "url",
+ },
+ // {
+ // text: "About",
+ // url: "/about",
+ // active: "url",
+ // },
+ // {
+ // type: "icon",
+ // icon: ,
+ // text: "Discord",
+ // url: "https://discord.gg/Qc2QTTH5dF",
+ // },
+ {
+ type: "icon",
+ icon: ,
+ text: "GitHub",
+ url: "https://github.com/TypeCellOS/BlockNote",
+ },
+ {
+ type: "custom",
+ on: "all",
+ secondary: true,
+ children: ,
+ },
+];
+export function baseOptions(): BaseLayoutProps {
+ return {
+ themeSwitch: {
+ enabled: false,
+ },
+ nav: {
+ title: (
+
+ ),
+ },
+ links,
+ };
+}
diff --git a/docs/lib/product-list.ts b/docs/lib/product-list.ts
new file mode 100644
index 0000000000..49e52033c7
--- /dev/null
+++ b/docs/lib/product-list.ts
@@ -0,0 +1,33 @@
+export const PRODUCTS = {
+ business: {
+ id:
+ process.env.NODE_ENV === "production"
+ ? "c7faaa4c-7805-4722-88d2-5a68f068d546"
+ : "8049f66f-fd0a-4690-a0aa-442ac5b03040",
+ name: "Business",
+ slug: "business",
+ } as const,
+ "business-yearly": {
+ id:
+ process.env.NODE_ENV === "production"
+ ? "ba3965dc-e1ca-494e-b36a-62e2e41615d4"
+ : "NOT-CREATED",
+ name: "Business Yearly",
+ slug: "business-yearly",
+ } as const,
+ starter: {
+ id:
+ process.env.NODE_ENV === "production"
+ ? "ef89c65b-9b18-4091-8de6-264554aa027e"
+ : "ab70fea5-172c-4aac-b3fc-0824f2a5b670",
+ name: "Starter",
+ slug: "starter",
+ } as const,
+ free: {
+ id: "00000000-0000-0000-0000-000000000000",
+ name: "Free",
+ slug: "free",
+ } as const,
+} as const;
+
+export type ProductSlug = (typeof PRODUCTS)[keyof typeof PRODUCTS]["slug"];
diff --git a/docs/lib/send-mail.tsx b/docs/lib/send-mail.tsx
new file mode 100644
index 0000000000..bc1331cf10
--- /dev/null
+++ b/docs/lib/send-mail.tsx
@@ -0,0 +1,95 @@
+import nodemailer from "nodemailer";
+import { render } from "@react-email/render";
+import MagicLinkEmail from "@/emails/magic-link";
+import VerifyEmail from "@/emails/verify-email";
+import WelcomeEmail from "@/emails/welcome";
+import * as Sentry from "@sentry/nextjs";
+
+const IS_SMTP_CONFIGURED =
+ process.env.SMTP_HOST &&
+ process.env.SMTP_PORT &&
+ process.env.SMTP_USER &&
+ process.env.SMTP_PASS;
+
+const transporter = nodemailer.createTransport({
+ host: String(process.env.SMTP_HOST),
+ port: Number(process.env.SMTP_PORT),
+ secure: process.env.SMTP_SECURE !== "false", // true for port 465, false for other ports
+ auth: {
+ user: String(process.env.SMTP_USER),
+ pass: String(process.env.SMTP_PASS),
+ },
+});
+
+const TEMPLATE_COMPONENTS = {
+ verifyEmail: {
+ subject: "BlockNote - Verify your email address",
+ component: VerifyEmail,
+ },
+ magicLink: {
+ subject: "BlockNote - Sign in to your account",
+ component: MagicLinkEmail,
+ },
+ welcome: {
+ subject: "BlockNote - Welcome to BlockNote",
+ component: WelcomeEmail,
+ },
+} as const;
+
+export async function sendEmail({
+ to,
+ template,
+ props,
+}: {
+ to: string;
+ template: T;
+ props: Parameters<(typeof TEMPLATE_COMPONENTS)[T]["component"]>[0];
+}) {
+ if (!IS_SMTP_CONFIGURED) {
+ if (process.env.NODE_ENV === "production") {
+ throw new Error(
+ "SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASS must be set to send emails",
+ );
+ }
+
+ console.log(
+ "No SMTP credentials found, skipping email: ",
+ await render(TEMPLATE_COMPONENTS[template].component(props), {
+ pretty: true,
+ plainText: true,
+ }),
+ );
+ return;
+ }
+ const text = await render(TEMPLATE_COMPONENTS[template].component(props), {
+ pretty: true,
+ plainText: true,
+ });
+
+ const html = await render(TEMPLATE_COMPONENTS[template].component(props), {
+ pretty: true,
+ });
+
+ const info = await new Promise(
+ (resolve, reject) =>
+ transporter.sendMail(
+ {
+ from: `"BlockNote" <${process.env.SMTP_FROM || process.env.SMTP_USER}>`,
+ to,
+ subject: TEMPLATE_COMPONENTS[template].subject,
+ text,
+ html,
+ },
+ (err, data) => {
+ if (err) {
+ Sentry.captureException(err);
+ reject(err);
+ return;
+ }
+ resolve(data);
+ },
+ ),
+ );
+
+ console.log("Email sent: ", info.messageId);
+}
diff --git a/docs/lib/source/docs.ts b/docs/lib/source/docs.ts
new file mode 100644
index 0000000000..140086837c
--- /dev/null
+++ b/docs/lib/source/docs.ts
@@ -0,0 +1,26 @@
+import { type InferPageType, loader } from "fumadocs-core/source";
+import { docs } from "fumadocs-mdx:collections/server";
+
+// See https://fumadocs.dev/docs/headless/source-api for more info
+export const source = loader({
+ baseUrl: "/docs",
+ source: docs.toFumadocsSource(),
+ plugins: [],
+});
+
+export function getPageImage(page: InferPageType) {
+ const segments = [...page.slugs, "image.png"];
+
+ return {
+ segments,
+ url: `/og/docs/${segments.join("/")}`,
+ };
+}
+
+export async function getLLMText(page: InferPageType) {
+ const processed = await page.data.getText("processed");
+
+ return `# ${page.data.title}
+
+${processed}`;
+}
diff --git a/docs/lib/source/examples.ts b/docs/lib/source/examples.ts
new file mode 100644
index 0000000000..0d86f2a5ce
--- /dev/null
+++ b/docs/lib/source/examples.ts
@@ -0,0 +1,26 @@
+import { type InferPageType, loader } from "fumadocs-core/source";
+import { examples } from "fumadocs-mdx:collections/server";
+
+// See https://fumadocs.dev/docs/headless/source-api for more info
+export const source = loader({
+ baseUrl: "/examples",
+ source: examples.toFumadocsSource(),
+ plugins: [],
+});
+
+export function getPageImage(page: InferPageType) {
+ const segments = [...page.slugs, "image.png"];
+
+ return {
+ segments,
+ url: `/og/examples/${segments.join("/")}`,
+ };
+}
+
+export async function getLLMText(page: InferPageType) {
+ const processed = await page.data.getText("processed");
+
+ return `# ${page.data.title}
+
+${processed}`;
+}
diff --git a/docs/lib/source/pages.ts b/docs/lib/source/pages.ts
new file mode 100644
index 0000000000..ada917c408
--- /dev/null
+++ b/docs/lib/source/pages.ts
@@ -0,0 +1,26 @@
+import { type InferPageType, loader } from "fumadocs-core/source";
+import { pages } from "fumadocs-mdx:collections/server";
+
+// See https://fumadocs.dev/docs/headless/source-api for more info
+export const source = loader({
+ baseUrl: "/",
+ source: pages.toFumadocsSource(),
+ plugins: [],
+});
+
+export function getPageImage(page: InferPageType) {
+ const segments = [...page.slugs, "image.png"];
+
+ return {
+ segments,
+ url: `/og/pages/${segments.join("/")}`,
+ };
+}
+
+export async function getLLMText(page: InferPageType) {
+ const processed = await page.data.getText("processed");
+
+ return `# ${page.data.title}
+
+${processed}`;
+}
diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx
new file mode 100644
index 0000000000..bdb79ee1f0
--- /dev/null
+++ b/docs/mdx-components.tsx
@@ -0,0 +1,48 @@
+import CTAButton from "@/components/CTAButton";
+import Example from "@/components/Example";
+import ThemedImage from "@/components/ThemedImage";
+import { getExampleData } from "@/lib/getExampleData";
+import * as Twoslash from "fumadocs-twoslash/ui";
+import {
+ createFileSystemGeneratorCache,
+ createGenerator,
+} from "fumadocs-typescript";
+import { AutoTypeTable } from "fumadocs-typescript/ui";
+import * as TabsComponents from "fumadocs-ui/components/tabs";
+import { TypeTable } from "fumadocs-ui/components/type-table";
+import defaultMdxComponents from "fumadocs-ui/mdx";
+import type { MDXComponents } from "mdx/types";
+import { Suspense } from "react";
+import GitHubButton from "./components/GitHubButton";
+
+const generator = createGenerator({
+ // set a cache, necessary for serverless platform like Vercel
+ cache: createFileSystemGeneratorCache(".next/fumadocs-typescript"),
+});
+
+export function getMDXComponents(components?: MDXComponents): MDXComponents {
+ return {
+ ...defaultMdxComponents,
+ ...Twoslash,
+ Example: (props: { name: string }) => {
+ const [exampleGroupName, exampleName] = props.name.split("/");
+
+ return (
+
+
+
+ );
+ },
+ ...TabsComponents,
+ ThemedImage: ThemedImage,
+ TypeTable: TypeTable,
+ AutoTypeTable: (props) => (
+
+ ),
+ CTAButton,
+ GitHubButton,
+ ...components,
+ };
+}
diff --git a/docs/next.config.ts b/docs/next.config.ts
new file mode 100644
index 0000000000..0879257822
--- /dev/null
+++ b/docs/next.config.ts
@@ -0,0 +1,74 @@
+import { withSentryConfig } from "@sentry/nextjs";
+import { createMDX } from "fumadocs-mdx/next";
+import { NextConfig } from "next";
+import { redirects } from "./redirects";
+
+const withMDX = createMDX();
+
+const config = {
+ // output: "export",
+ reactStrictMode: true,
+ serverExternalPackages: ["typescript", "twoslash"],
+ reactCompiler: true,
+ redirects,
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com",
+ port: "",
+ pathname: "/u/**",
+ },
+ {
+ protocol: "https",
+ hostname: "github.com",
+ port: "",
+ pathname: "/**",
+ },
+ ],
+ },
+} satisfies NextConfig;
+
+export default withSentryConfig(withMDX(config), {
+ // For all available options, see:
+ // https://www.npmjs.com/package/@sentry/webpack-plugin#options
+
+ org: "blocknote-js",
+
+ project: "website",
+
+ // Only print logs for uploading source maps in CI
+ silent: !process.env.CI,
+
+ // For all available options, see:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
+
+ // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
+ // This can increase your server load as well as your hosting bill.
+ // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
+ // side errors will fail.
+ tunnelRoute: "/monitoring",
+
+ webpack: {
+ // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
+ // See the following for more information:
+ // https://docs.sentry.io/product/crons/
+ // https://vercel.com/docs/cron-jobs
+ automaticVercelMonitors: true,
+
+ // TODO: why was this disabled?
+ reactComponentAnnotation: {
+ enabled: false,
+ },
+ // Tree-shaking options for reducing bundle size
+ treeshake: {
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ removeDebugLogging: true,
+ },
+ },
+
+ telemetry: false,
+});
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 0000000000..3e64e6b44b
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,133 @@
+{
+ "name": "docs",
+ "version": "0.0.0",
+ "private": true,
+ "license": "UNLICENSED",
+ "scripts": {
+ "dev": "next dev",
+ "dev:email": "next dev",
+ "prebuild:site": "nx run @blocknote/dev-scripts:gen",
+ "build:site": "fumadocs-mdx && next build",
+ "start": "next start",
+ "types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
+ "postinstall": "fumadocs-mdx",
+ "lint": "eslint",
+ "init-db": "pnpx @better-auth/cli migrate",
+ "test": "pnpm -w run gen && node validate-links.mjs"
+ },
+ "dependencies": {
+ "@ai-sdk/groq": "^3.0.2",
+ "@aws-sdk/client-s3": "^3.609.0",
+ "@aws-sdk/s3-request-presigner": "^3.609.0",
+ "@base-ui/react": "^1.1.0",
+ "@blocknote/ariakit": "workspace:*",
+ "@blocknote/code-block": "workspace:*",
+ "@blocknote/core": "workspace:*",
+ "@blocknote/mantine": "workspace:*",
+ "@blocknote/react": "workspace:*",
+ "@blocknote/server-util": "workspace:*",
+ "@blocknote/shadcn": "workspace:*",
+ "@blocknote/xl-ai": "workspace:*",
+ "@blocknote/xl-docx-exporter": "workspace:*",
+ "@blocknote/xl-email-exporter": "workspace:*",
+ "@blocknote/xl-multi-column": "workspace:*",
+ "@blocknote/xl-odt-exporter": "workspace:*",
+ "@blocknote/xl-pdf-exporter": "workspace:*",
+ "@fumadocs/base-ui": "16.5.0",
+ "@liveblocks/client": "^3.17.0",
+ "@liveblocks/react": "^3.17.0",
+ "@liveblocks/react-blocknote": "^3.17.0",
+ "@liveblocks/react-tiptap": "^3.17.0",
+ "@liveblocks/react-ui": "^3.17.0",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "@marsidev/react-turnstile": "^1.4.2",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.1",
+ "@orama/orama": "^3.1.18",
+ "@polar-sh/better-auth": "^1.6.4",
+ "@polar-sh/sdk": "^0.42.2",
+ "@react-email/components": "^1.0.4",
+ "@react-email/render": "^2.0.4",
+ "@react-pdf/renderer": "^4.3.0",
+ "@sentry/nextjs": "^10.34.0",
+ "@shikijs/core": "^4",
+ "@shikijs/engine-javascript": "^4",
+ "@shikijs/langs-precompiled": "^4",
+ "@shikijs/themes": "^4",
+ "@shikijs/types": "^4",
+ "@tiptap/core": "^3.13.0",
+ "@uppy/core": "^3.13.1",
+ "@uppy/dashboard": "^3.9.1",
+ "@uppy/drag-drop": "^3.1.1",
+ "@uppy/file-input": "^3.1.2",
+ "@uppy/image-editor": "^2.4.6",
+ "@uppy/progress-bar": "^3.1.1",
+ "@uppy/react": "^3.4.0",
+ "@uppy/screen-capture": "^3.2.0",
+ "@uppy/status-bar": "^3.1.1",
+ "@uppy/webcam": "^3.4.2",
+ "@uppy/xhr-upload": "^3.4.0",
+ "@vercel/analytics": "^1.6.1",
+ "@y-sweet/react": "^0.6.3",
+ "ai": "^6.0.5",
+ "better-auth": "~1.4.15",
+ "better-sqlite3": "^12.6.2",
+ "class-variance-authority": "^0.7.1",
+ "framer-motion": "^12.26.2",
+ "fumadocs-core": "16.5.0",
+ "fumadocs-mdx": "^14.2.6",
+ "fumadocs-twoslash": "^3.1.12",
+ "fumadocs-typescript": "^5.1.1",
+ "fumadocs-ui": "npm:@fumadocs/base-ui@16.5.0",
+ "lucide-react": "^0.562.0",
+ "motion": "^12.28.1",
+ "next": "^16.2.6",
+ "next-themes": "^0.4.6",
+ "nodemailer": "^7.0.12",
+ "pg": "^8.17.1",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "react-email": "^5.2.5",
+ "react-github-btn": "^1.4.0",
+ "react-icons": "^5.5.0",
+ "react-use-measure": "^2.1.7",
+ "scroll-into-view-if-needed": "^3.1.0",
+ "shiki": "^4",
+ "tailwind-merge": "^3.4.0",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27",
+ "zod": "^4.3.5"
+ },
+ "devDependencies": {
+ "@blocknote/code-block": "workspace:*",
+ "@blocknote/core": "workspace:*",
+ "@blocknote/mantine": "workspace:*",
+ "@blocknote/react": "workspace:*",
+ "@blocknote/server-util": "workspace:*",
+ "@blocknote/shadcn": "workspace:*",
+ "@blocknote/xl-ai": "workspace:*",
+ "@blocknote/xl-docx-exporter": "workspace:*",
+ "@blocknote/xl-email-exporter": "workspace:*",
+ "@blocknote/xl-multi-column": "workspace:*",
+ "@blocknote/xl-odt-exporter": "workspace:*",
+ "@blocknote/xl-pdf-exporter": "workspace:*",
+ "@tailwindcss/postcss": "^4.1.18",
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/mdx": "^2.0.13",
+ "@types/node": "^25.0.5",
+ "@types/nodemailer": "^7.0.5",
+ "@types/pg": "^8.16.0",
+ "@types/react": "^19.2.8",
+ "@types/react-dom": "^19.2.3",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "eslint": "^9.39.2",
+ "eslint-config-next": "^16.2.6",
+ "next-validate-link": "^1.6.4",
+ "postcss": "^8.5.6",
+ "serve": "^14.2.6",
+ "tailwindcss": "^4.1.18",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5.9.3"
+ }
+}
\ No newline at end of file
diff --git a/docs/partykitserver.ts.bak b/docs/partykitserver.ts.bak
new file mode 100644
index 0000000000..03a12a2061
--- /dev/null
+++ b/docs/partykitserver.ts.bak
@@ -0,0 +1,32 @@
+import type * as Party from "partykit/server";
+import { onConnect } from "y-partykit";
+
+const EXPIRY_PERIOD_MILLISECONDS = 60 * 60 * 1000; // 1 hour
+
+// deploy with npx partykit deploy partykitserver.ts --name blocknote
+// preview with npx partykit dev partykitserver.ts
+export default class Server implements Party.Server {
+ constructor(readonly party: Party.Party) {}
+
+ onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
+ // A websocket just connected!
+
+ return onConnect(conn, this.party, { persist: false, gc: true });
+ }
+
+ async onMessage(message: string) {
+ // const data = JSON.parse(message);
+ // do something, and save to storage
+ // await this.party.storage.put(data.id, data);
+ // console.log("on message");
+ await this.party.storage.setAlarm(Date.now() + EXPIRY_PERIOD_MILLISECONDS);
+ }
+
+ async onAlarm() {
+ // clear all storage in this room
+ console.log("alarm, delete storage");
+ await this.party.storage.deleteAll();
+ }
+}
+
+Server satisfies Party.Worker;
diff --git a/docs/postcss.config.mjs b/docs/postcss.config.mjs
new file mode 100644
index 0000000000..61e36849cf
--- /dev/null
+++ b/docs/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/docs/proxy.ts b/docs/proxy.ts
new file mode 100644
index 0000000000..7c49c04b6d
--- /dev/null
+++ b/docs/proxy.ts
@@ -0,0 +1,29 @@
+import { getLLMText, source } from "@/lib/source/docs";
+import type { NextRequest } from "next/server";
+
+export async function proxy(request: NextRequest) {
+ const { pathname } = request.nextUrl;
+
+ if (!pathname.endsWith(".md")) {
+ return;
+ }
+
+ const slug = pathname.slice(0, -3).split("/").filter(Boolean).slice(1);
+
+ const page = source.getPage(slug);
+ if (!page) {
+ return new Response("Not Found", { status: 404 });
+ }
+
+ const markdown = await getLLMText(page);
+
+ return new Response(markdown, {
+ headers: {
+ "Content-Type": "text/markdown; charset=utf-8",
+ },
+ });
+}
+
+export const config = {
+ matcher: ["/docs/:path*", "/docs", "/docs.md"],
+};
diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png
new file mode 100644
index 0000000000..93b66fb5e6
Binary files /dev/null and b/docs/public/apple-touch-icon.png differ
diff --git a/docs/public/avatars/avatar1-f.jpg b/docs/public/avatars/avatar1-f.jpg
new file mode 100644
index 0000000000..8a23b8f7ee
Binary files /dev/null and b/docs/public/avatars/avatar1-f.jpg differ
diff --git a/docs/public/avatars/avatar10-m.jpg b/docs/public/avatars/avatar10-m.jpg
new file mode 100644
index 0000000000..5cf056af45
Binary files /dev/null and b/docs/public/avatars/avatar10-m.jpg differ
diff --git a/docs/public/avatars/avatar11-m.jpg b/docs/public/avatars/avatar11-m.jpg
new file mode 100644
index 0000000000..be900eccde
Binary files /dev/null and b/docs/public/avatars/avatar11-m.jpg differ
diff --git a/docs/public/avatars/avatar12-f.jpg b/docs/public/avatars/avatar12-f.jpg
new file mode 100644
index 0000000000..04f02eb129
Binary files /dev/null and b/docs/public/avatars/avatar12-f.jpg differ
diff --git a/docs/public/avatars/avatar3-m.jpg b/docs/public/avatars/avatar3-m.jpg
new file mode 100644
index 0000000000..0d29ca0ed2
Binary files /dev/null and b/docs/public/avatars/avatar3-m.jpg differ
diff --git a/docs/public/avatars/avatar4-f.jpg b/docs/public/avatars/avatar4-f.jpg
new file mode 100644
index 0000000000..5d13b390d4
Binary files /dev/null and b/docs/public/avatars/avatar4-f.jpg differ
diff --git a/docs/public/avatars/avatar6-f.jpg b/docs/public/avatars/avatar6-f.jpg
new file mode 100644
index 0000000000..1db67d57ba
Binary files /dev/null and b/docs/public/avatars/avatar6-f.jpg differ
diff --git a/docs/public/avatars/avatar7-m.jpg b/docs/public/avatars/avatar7-m.jpg
new file mode 100644
index 0000000000..b4a181dec2
Binary files /dev/null and b/docs/public/avatars/avatar7-m.jpg differ
diff --git a/docs/public/avatars/avatar9-f.jpg b/docs/public/avatars/avatar9-f.jpg
new file mode 100644
index 0000000000..ddb289ac3f
Binary files /dev/null and b/docs/public/avatars/avatar9-f.jpg differ
diff --git a/docs/public/avatars/avater2-m.jpg b/docs/public/avatars/avater2-m.jpg
new file mode 100644
index 0000000000..7b57a3e55c
Binary files /dev/null and b/docs/public/avatars/avater2-m.jpg differ
diff --git a/docs/public/avatars/avater5-m.jpg b/docs/public/avatars/avater5-m.jpg
new file mode 100644
index 0000000000..3006614748
Binary files /dev/null and b/docs/public/avatars/avater5-m.jpg differ
diff --git a/docs/public/avatars/avater8-f.jpg b/docs/public/avatars/avater8-f.jpg
new file mode 100644
index 0000000000..aab0b4d5d4
Binary files /dev/null and b/docs/public/avatars/avater8-f.jpg differ
diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico
new file mode 100644
index 0000000000..bf1649b1bc
Binary files /dev/null and b/docs/public/favicon.ico differ
diff --git a/docs/public/favicon.png b/docs/public/favicon.png
new file mode 100644
index 0000000000..bf1649b1bc
Binary files /dev/null and b/docs/public/favicon.png differ
diff --git a/packages/website/docs/public/img/logos/icon_light_500.svg b/docs/public/favicon.svg
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_500.svg
rename to docs/public/favicon.svg
diff --git a/docs/public/hero-demo.gif b/docs/public/hero-demo.gif
new file mode 100644
index 0000000000..c014e29a62
Binary files /dev/null and b/docs/public/hero-demo.gif differ
diff --git a/docs/public/hero-demo.png b/docs/public/hero-demo.png
new file mode 100644
index 0000000000..89a5427daa
Binary files /dev/null and b/docs/public/hero-demo.png differ
diff --git a/packages/website/docs/public/img/assets/try.dark.svg b/docs/public/img/assets/try.dark.svg
similarity index 100%
rename from packages/website/docs/public/img/assets/try.dark.svg
rename to docs/public/img/assets/try.dark.svg
diff --git a/packages/website/docs/public/img/assets/try.svg b/docs/public/img/assets/try.svg
similarity index 100%
rename from packages/website/docs/public/img/assets/try.svg
rename to docs/public/img/assets/try.svg
diff --git a/docs/public/img/features/block_based_design_dark.gif b/docs/public/img/features/block_based_design_dark.gif
new file mode 100644
index 0000000000..b0738eddb4
Binary files /dev/null and b/docs/public/img/features/block_based_design_dark.gif differ
diff --git a/docs/public/img/features/block_based_design_light.gif b/docs/public/img/features/block_based_design_light.gif
new file mode 100644
index 0000000000..b5cf60b826
Binary files /dev/null and b/docs/public/img/features/block_based_design_light.gif differ
diff --git a/docs/public/img/features/collaboration_dark.gif b/docs/public/img/features/collaboration_dark.gif
new file mode 100644
index 0000000000..71581af4b5
Binary files /dev/null and b/docs/public/img/features/collaboration_dark.gif differ
diff --git a/docs/public/img/features/collaboration_light.gif b/docs/public/img/features/collaboration_light.gif
new file mode 100644
index 0000000000..69d7eea546
Binary files /dev/null and b/docs/public/img/features/collaboration_light.gif differ
diff --git a/docs/public/img/features/works_out_of_the_box_dark.gif b/docs/public/img/features/works_out_of_the_box_dark.gif
new file mode 100644
index 0000000000..b99da9a6df
Binary files /dev/null and b/docs/public/img/features/works_out_of_the_box_dark.gif differ
diff --git a/docs/public/img/features/works_out_of_the_box_light.gif b/docs/public/img/features/works_out_of_the_box_light.gif
new file mode 100644
index 0000000000..ca1af41b50
Binary files /dev/null and b/docs/public/img/features/works_out_of_the_box_light.gif differ
diff --git a/packages/website/docs/public/img/logos/banner.dark.svg b/docs/public/img/logos/banner.dark.svg
similarity index 100%
rename from packages/website/docs/public/img/logos/banner.dark.svg
rename to docs/public/img/logos/banner.dark.svg
diff --git a/packages/website/docs/public/img/logos/banner.png b/docs/public/img/logos/banner.png
similarity index 100%
rename from packages/website/docs/public/img/logos/banner.png
rename to docs/public/img/logos/banner.png
diff --git a/packages/website/docs/public/img/logos/banner.svg b/docs/public/img/logos/banner.svg
similarity index 100%
rename from packages/website/docs/public/img/logos/banner.svg
rename to docs/public/img/logos/banner.svg
diff --git a/packages/website/docs/public/img/logos/banner@2x.png b/docs/public/img/logos/banner@2x.png
similarity index 100%
rename from packages/website/docs/public/img/logos/banner@2x.png
rename to docs/public/img/logos/banner@2x.png
diff --git a/packages/website/docs/public/img/logos/icon_light_400.png b/docs/public/img/logos/icon_light_400.png
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_400.png
rename to docs/public/img/logos/icon_light_400.png
diff --git a/packages/website/docs/public/img/logos/icon_light_400.svg b/docs/public/img/logos/icon_light_400.svg
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_400.svg
rename to docs/public/img/logos/icon_light_400.svg
diff --git a/packages/website/docs/public/img/logos/icon_light_400@2x.png b/docs/public/img/logos/icon_light_400@2x.png
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_400@2x.png
rename to docs/public/img/logos/icon_light_400@2x.png
diff --git a/packages/website/docs/public/img/logos/icon_light_500.png b/docs/public/img/logos/icon_light_500.png
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_500.png
rename to docs/public/img/logos/icon_light_500.png
diff --git a/docs/public/img/logos/icon_light_500.svg b/docs/public/img/logos/icon_light_500.svg
new file mode 100644
index 0000000000..2382d984e5
--- /dev/null
+++ b/docs/public/img/logos/icon_light_500.svg
@@ -0,0 +1,14 @@
+
diff --git a/packages/website/docs/public/img/logos/icon_light_500@2x.png b/docs/public/img/logos/icon_light_500@2x.png
similarity index 100%
rename from packages/website/docs/public/img/logos/icon_light_500@2x.png
rename to docs/public/img/logos/icon_light_500@2x.png
diff --git a/docs/public/img/screenshots/ai-menu-dark.png b/docs/public/img/screenshots/ai-menu-dark.png
new file mode 100644
index 0000000000..dd98e99f5f
Binary files /dev/null and b/docs/public/img/screenshots/ai-menu-dark.png differ
diff --git a/docs/public/img/screenshots/ai-menu.png b/docs/public/img/screenshots/ai-menu.png
new file mode 100644
index 0000000000..c556181d77
Binary files /dev/null and b/docs/public/img/screenshots/ai-menu.png differ
diff --git a/docs/public/img/screenshots/block_structure.png b/docs/public/img/screenshots/block_structure.png
new file mode 100644
index 0000000000..5b1c0af16a
Binary files /dev/null and b/docs/public/img/screenshots/block_structure.png differ
diff --git a/docs/public/img/screenshots/block_structure_dark.png b/docs/public/img/screenshots/block_structure_dark.png
new file mode 100644
index 0000000000..515fa97312
Binary files /dev/null and b/docs/public/img/screenshots/block_structure_dark.png differ
diff --git a/docs/public/img/screenshots/blocknote-ai.mp4 b/docs/public/img/screenshots/blocknote-ai.mp4
new file mode 100644
index 0000000000..a9f3cd7d2a
Binary files /dev/null and b/docs/public/img/screenshots/blocknote-ai.mp4 differ
diff --git a/packages/website/docs/public/img/screenshots/bullet_list_item_type.png b/docs/public/img/screenshots/bullet_list_item_type.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/bullet_list_item_type.png
rename to docs/public/img/screenshots/bullet_list_item_type.png
diff --git a/docs/public/img/screenshots/bullet_list_item_type_dark.png b/docs/public/img/screenshots/bullet_list_item_type_dark.png
new file mode 100644
index 0000000000..53b9b4f105
Binary files /dev/null and b/docs/public/img/screenshots/bullet_list_item_type_dark.png differ
diff --git a/packages/website/docs/public/img/screenshots/drag_handle_menu.png b/docs/public/img/screenshots/drag_handle_menu.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/drag_handle_menu.png
rename to docs/public/img/screenshots/drag_handle_menu.png
diff --git a/docs/public/img/screenshots/drag_handle_menu_dark.png b/docs/public/img/screenshots/drag_handle_menu_dark.png
new file mode 100644
index 0000000000..f149cbfecf
Binary files /dev/null and b/docs/public/img/screenshots/drag_handle_menu_dark.png differ
diff --git a/docs/public/img/screenshots/emoji_picker.png b/docs/public/img/screenshots/emoji_picker.png
new file mode 100644
index 0000000000..d912a299de
Binary files /dev/null and b/docs/public/img/screenshots/emoji_picker.png differ
diff --git a/docs/public/img/screenshots/emoji_picker_dark.png b/docs/public/img/screenshots/emoji_picker_dark.png
new file mode 100644
index 0000000000..eb55c7a0b8
Binary files /dev/null and b/docs/public/img/screenshots/emoji_picker_dark.png differ
diff --git a/packages/website/docs/public/img/screenshots/formatting_toolbar.png b/docs/public/img/screenshots/formatting_toolbar.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/formatting_toolbar.png
rename to docs/public/img/screenshots/formatting_toolbar.png
diff --git a/docs/public/img/screenshots/formatting_toolbar_dark.png b/docs/public/img/screenshots/formatting_toolbar_dark.png
new file mode 100644
index 0000000000..e08048d7d6
Binary files /dev/null and b/docs/public/img/screenshots/formatting_toolbar_dark.png differ
diff --git a/packages/website/docs/public/img/screenshots/heading_type.png b/docs/public/img/screenshots/heading_type.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/heading_type.png
rename to docs/public/img/screenshots/heading_type.png
diff --git a/docs/public/img/screenshots/heading_type_dark.png b/docs/public/img/screenshots/heading_type_dark.png
new file mode 100644
index 0000000000..c0f4057552
Binary files /dev/null and b/docs/public/img/screenshots/heading_type_dark.png differ
diff --git a/docs/public/img/screenshots/home/any_model.png b/docs/public/img/screenshots/home/any_model.png
new file mode 100644
index 0000000000..7702c27a5f
Binary files /dev/null and b/docs/public/img/screenshots/home/any_model.png differ
diff --git a/docs/public/img/screenshots/home/bring_any_model_light_cropped.png b/docs/public/img/screenshots/home/bring_any_model_light_cropped.png
new file mode 100644
index 0000000000..4d6a60810f
Binary files /dev/null and b/docs/public/img/screenshots/home/bring_any_model_light_cropped.png differ
diff --git a/docs/public/img/screenshots/home/code-typescript-support.png b/docs/public/img/screenshots/home/code-typescript-support.png
new file mode 100644
index 0000000000..aeedcd92ec
Binary files /dev/null and b/docs/public/img/screenshots/home/code-typescript-support.png differ
diff --git a/docs/public/img/screenshots/home/comments.png b/docs/public/img/screenshots/home/comments.png
new file mode 100644
index 0000000000..505faa582a
Binary files /dev/null and b/docs/public/img/screenshots/home/comments.png differ
diff --git a/docs/public/img/screenshots/home/human_in_the_loop.png b/docs/public/img/screenshots/home/human_in_the_loop.png
new file mode 100644
index 0000000000..1be95ad79f
Binary files /dev/null and b/docs/public/img/screenshots/home/human_in_the_loop.png differ
diff --git a/docs/public/img/screenshots/home/versioning.png b/docs/public/img/screenshots/home/versioning.png
new file mode 100644
index 0000000000..75648e4c55
Binary files /dev/null and b/docs/public/img/screenshots/home/versioning.png differ
diff --git a/docs/public/img/screenshots/home/versioning_feature_light_1769531547834.png b/docs/public/img/screenshots/home/versioning_feature_light_1769531547834.png
new file mode 100644
index 0000000000..8a2616a132
Binary files /dev/null and b/docs/public/img/screenshots/home/versioning_feature_light_1769531547834.png differ
diff --git a/docs/public/img/screenshots/image_toolbar.png b/docs/public/img/screenshots/image_toolbar.png
new file mode 100644
index 0000000000..b19ead973a
Binary files /dev/null and b/docs/public/img/screenshots/image_toolbar.png differ
diff --git a/docs/public/img/screenshots/image_toolbar_dark.png b/docs/public/img/screenshots/image_toolbar_dark.png
new file mode 100644
index 0000000000..ed5c6476ee
Binary files /dev/null and b/docs/public/img/screenshots/image_toolbar_dark.png differ
diff --git a/docs/public/img/screenshots/image_type.png b/docs/public/img/screenshots/image_type.png
new file mode 100644
index 0000000000..2e66f60dca
Binary files /dev/null and b/docs/public/img/screenshots/image_type.png differ
diff --git a/docs/public/img/screenshots/image_type_dark.png b/docs/public/img/screenshots/image_type_dark.png
new file mode 100644
index 0000000000..66d2feab5d
Binary files /dev/null and b/docs/public/img/screenshots/image_type_dark.png differ
diff --git a/docs/public/img/screenshots/link_toolbar.png b/docs/public/img/screenshots/link_toolbar.png
new file mode 100644
index 0000000000..320e577fa8
Binary files /dev/null and b/docs/public/img/screenshots/link_toolbar.png differ
diff --git a/docs/public/img/screenshots/link_toolbar_dark.png b/docs/public/img/screenshots/link_toolbar_dark.png
new file mode 100644
index 0000000000..8468d6b417
Binary files /dev/null and b/docs/public/img/screenshots/link_toolbar_dark.png differ
diff --git a/docs/public/img/screenshots/liveblocks_blocknote_example.mp4 b/docs/public/img/screenshots/liveblocks_blocknote_example.mp4
new file mode 100644
index 0000000000..531acd53d5
Binary files /dev/null and b/docs/public/img/screenshots/liveblocks_blocknote_example.mp4 differ
diff --git a/packages/website/docs/public/img/screenshots/numbered_list_item_type.png b/docs/public/img/screenshots/numbered_list_item_type.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/numbered_list_item_type.png
rename to docs/public/img/screenshots/numbered_list_item_type.png
diff --git a/docs/public/img/screenshots/numbered_list_item_type_dark.png b/docs/public/img/screenshots/numbered_list_item_type_dark.png
new file mode 100644
index 0000000000..2cec7dc1b6
Binary files /dev/null and b/docs/public/img/screenshots/numbered_list_item_type_dark.png differ
diff --git a/packages/website/docs/public/img/screenshots/paragraph_type.png b/docs/public/img/screenshots/paragraph_type.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/paragraph_type.png
rename to docs/public/img/screenshots/paragraph_type.png
diff --git a/docs/public/img/screenshots/paragraph_type_dark.png b/docs/public/img/screenshots/paragraph_type_dark.png
new file mode 100644
index 0000000000..39a8413272
Binary files /dev/null and b/docs/public/img/screenshots/paragraph_type_dark.png differ
diff --git a/packages/website/docs/public/img/screenshots/side_menu.png b/docs/public/img/screenshots/side_menu.png
similarity index 100%
rename from packages/website/docs/public/img/screenshots/side_menu.png
rename to docs/public/img/screenshots/side_menu.png
diff --git a/docs/public/img/screenshots/side_menu_dark.png b/docs/public/img/screenshots/side_menu_dark.png
new file mode 100644
index 0000000000..3828672ca1
Binary files /dev/null and b/docs/public/img/screenshots/side_menu_dark.png differ
diff --git a/docs/public/img/screenshots/slash_menu.png b/docs/public/img/screenshots/slash_menu.png
new file mode 100644
index 0000000000..d301d29ad8
Binary files /dev/null and b/docs/public/img/screenshots/slash_menu.png differ
diff --git a/docs/public/img/screenshots/slash_menu_dark.png b/docs/public/img/screenshots/slash_menu_dark.png
new file mode 100644
index 0000000000..394dcc5715
Binary files /dev/null and b/docs/public/img/screenshots/slash_menu_dark.png differ
diff --git a/docs/public/img/screenshots/table_type.png b/docs/public/img/screenshots/table_type.png
new file mode 100644
index 0000000000..11921f6a82
Binary files /dev/null and b/docs/public/img/screenshots/table_type.png differ
diff --git a/docs/public/img/screenshots/table_type_dark.png b/docs/public/img/screenshots/table_type_dark.png
new file mode 100644
index 0000000000..88b1c952f7
Binary files /dev/null and b/docs/public/img/screenshots/table_type_dark.png differ
diff --git a/docs/public/img/sponsors/agree.png b/docs/public/img/sponsors/agree.png
new file mode 100644
index 0000000000..21673b667d
Binary files /dev/null and b/docs/public/img/sponsors/agree.png differ
diff --git a/docs/public/img/sponsors/atuin.png b/docs/public/img/sponsors/atuin.png
new file mode 100644
index 0000000000..6a4c5ac3de
Binary files /dev/null and b/docs/public/img/sponsors/atuin.png differ
diff --git a/docs/public/img/sponsors/capitolDark.svg b/docs/public/img/sponsors/capitolDark.svg
new file mode 100644
index 0000000000..357d0e20bf
--- /dev/null
+++ b/docs/public/img/sponsors/capitolDark.svg
@@ -0,0 +1,19 @@
+
diff --git a/docs/public/img/sponsors/capitolLight.svg b/docs/public/img/sponsors/capitolLight.svg
new file mode 100644
index 0000000000..3e7b6c1de2
--- /dev/null
+++ b/docs/public/img/sponsors/capitolLight.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/docs/public/img/sponsors/cella.png b/docs/public/img/sponsors/cella.png
new file mode 100644
index 0000000000..b3a7ef8b1f
Binary files /dev/null and b/docs/public/img/sponsors/cella.png differ
diff --git a/docs/public/img/sponsors/claimer.svg b/docs/public/img/sponsors/claimer.svg
new file mode 100644
index 0000000000..c2bdaba5f3
--- /dev/null
+++ b/docs/public/img/sponsors/claimer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/img/sponsors/deepOrigin.svg b/docs/public/img/sponsors/deepOrigin.svg
new file mode 100644
index 0000000000..a366bb26b1
--- /dev/null
+++ b/docs/public/img/sponsors/deepOrigin.svg
@@ -0,0 +1,29 @@
+
diff --git a/docs/public/img/sponsors/dinumDark.svg b/docs/public/img/sponsors/dinumDark.svg
new file mode 100644
index 0000000000..633c37d8b0
--- /dev/null
+++ b/docs/public/img/sponsors/dinumDark.svg
@@ -0,0 +1,181 @@
+
+
diff --git a/docs/public/img/sponsors/dinumLight.svg b/docs/public/img/sponsors/dinumLight.svg
new file mode 100644
index 0000000000..d6f9a3d91c
--- /dev/null
+++ b/docs/public/img/sponsors/dinumLight.svg
@@ -0,0 +1,159 @@
+
+
diff --git a/docs/public/img/sponsors/illumi.png b/docs/public/img/sponsors/illumi.png
new file mode 100644
index 0000000000..71420ce4f3
Binary files /dev/null and b/docs/public/img/sponsors/illumi.png differ
diff --git a/docs/public/img/sponsors/juma.svg b/docs/public/img/sponsors/juma.svg
new file mode 100644
index 0000000000..6a56586456
--- /dev/null
+++ b/docs/public/img/sponsors/juma.svg
@@ -0,0 +1,15 @@
+
diff --git a/docs/public/img/sponsors/krisp.svg b/docs/public/img/sponsors/krisp.svg
new file mode 100644
index 0000000000..9bca7b88cb
--- /dev/null
+++ b/docs/public/img/sponsors/krisp.svg
@@ -0,0 +1,8 @@
+
diff --git a/docs/public/img/sponsors/lasuite-docs.svg b/docs/public/img/sponsors/lasuite-docs.svg
new file mode 100644
index 0000000000..7c89ab89e2
--- /dev/null
+++ b/docs/public/img/sponsors/lasuite-docs.svg
@@ -0,0 +1,16 @@
+
diff --git a/docs/public/img/sponsors/nlnetDark.svg b/docs/public/img/sponsors/nlnetDark.svg
new file mode 100644
index 0000000000..a6ef93ac76
--- /dev/null
+++ b/docs/public/img/sponsors/nlnetDark.svg
@@ -0,0 +1,75 @@
+
+
+
diff --git a/packages/website/docs/public/img/sponsors/nlnet.svg b/docs/public/img/sponsors/nlnetLight.svg
similarity index 100%
rename from packages/website/docs/public/img/sponsors/nlnet.svg
rename to docs/public/img/sponsors/nlnetLight.svg
diff --git a/docs/public/img/sponsors/notePlanDark.png b/docs/public/img/sponsors/notePlanDark.png
new file mode 100644
index 0000000000..c0d750cbea
Binary files /dev/null and b/docs/public/img/sponsors/notePlanDark.png differ
diff --git a/docs/public/img/sponsors/notePlanLight.png b/docs/public/img/sponsors/notePlanLight.png
new file mode 100644
index 0000000000..fe4a929eff
Binary files /dev/null and b/docs/public/img/sponsors/notePlanLight.png differ
diff --git a/docs/public/img/sponsors/openproject.svg b/docs/public/img/sponsors/openproject.svg
new file mode 100644
index 0000000000..229d651732
--- /dev/null
+++ b/docs/public/img/sponsors/openproject.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/img/sponsors/poggioDark.svg b/docs/public/img/sponsors/poggioDark.svg
new file mode 100644
index 0000000000..227b770c60
--- /dev/null
+++ b/docs/public/img/sponsors/poggioDark.svg
@@ -0,0 +1,10 @@
+
diff --git a/docs/public/img/sponsors/poggioLight.svg b/docs/public/img/sponsors/poggioLight.svg
new file mode 100644
index 0000000000..6d4a61de3c
--- /dev/null
+++ b/docs/public/img/sponsors/poggioLight.svg
@@ -0,0 +1,10 @@
+
diff --git a/docs/public/img/sponsors/semrush.dark.png b/docs/public/img/sponsors/semrush.dark.png
new file mode 100644
index 0000000000..473d544208
Binary files /dev/null and b/docs/public/img/sponsors/semrush.dark.png differ
diff --git a/docs/public/img/sponsors/semrush.light.png b/docs/public/img/sponsors/semrush.light.png
new file mode 100644
index 0000000000..bce4dc6975
Binary files /dev/null and b/docs/public/img/sponsors/semrush.light.png differ
diff --git a/docs/public/img/sponsors/ten.webp b/docs/public/img/sponsors/ten.webp
new file mode 100644
index 0000000000..6b6722a9d6
Binary files /dev/null and b/docs/public/img/sponsors/ten.webp differ
diff --git a/docs/public/img/sponsors/twentyDark.png b/docs/public/img/sponsors/twentyDark.png
new file mode 100644
index 0000000000..81a50e6bc7
Binary files /dev/null and b/docs/public/img/sponsors/twentyDark.png differ
diff --git a/docs/public/img/sponsors/twentyLight.png b/docs/public/img/sponsors/twentyLight.png
new file mode 100644
index 0000000000..79566d3a7e
Binary files /dev/null and b/docs/public/img/sponsors/twentyLight.png differ
diff --git a/docs/public/img/sponsors/zendis.svg b/docs/public/img/sponsors/zendis.svg
new file mode 100644
index 0000000000..7cdafa9098
--- /dev/null
+++ b/docs/public/img/sponsors/zendis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/site.webmanifest b/docs/public/site.webmanifest
new file mode 100644
index 0000000000..198c5b3a6c
--- /dev/null
+++ b/docs/public/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "BlockNote",
+ "short_name": "BlockNote",
+ "description": "A beautiful text editor that just works. Easily add an editor to your app that users will love.",
+ "icons": [
+ {
+ "src": "/favicon.png",
+ "sizes": "500x500",
+ "type": "image/png"
+ },
+ {
+ "src": "/apple-touch-icon.png",
+ "sizes": "500x500",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#667eea",
+ "background_color": "#ffffff",
+ "display": "standalone",
+ "start_url": "/"
+}
diff --git a/docs/public/video/ai-select.mp4 b/docs/public/video/ai-select.mp4
new file mode 100644
index 0000000000..203f581ceb
Binary files /dev/null and b/docs/public/video/ai-select.mp4 differ
diff --git a/docs/public/video/batteries-included.mp4 b/docs/public/video/batteries-included.mp4
new file mode 100644
index 0000000000..1dce62244e
Binary files /dev/null and b/docs/public/video/batteries-included.mp4 differ
diff --git a/docs/public/video/blocknote-explainer.mp4 b/docs/public/video/blocknote-explainer.mp4
new file mode 100644
index 0000000000..15349fc82b
Binary files /dev/null and b/docs/public/video/blocknote-explainer.mp4 differ
diff --git a/docs/public/video/docs-poster.png b/docs/public/video/docs-poster.png
new file mode 100644
index 0000000000..654f2e7689
Binary files /dev/null and b/docs/public/video/docs-poster.png differ
diff --git a/docs/public/video/docs.mp4 b/docs/public/video/docs.mp4
new file mode 100644
index 0000000000..56f2726341
Binary files /dev/null and b/docs/public/video/docs.mp4 differ
diff --git a/docs/public/video/dragdrop.mp4 b/docs/public/video/dragdrop.mp4
new file mode 100644
index 0000000000..d0b5339306
Binary files /dev/null and b/docs/public/video/dragdrop.mp4 differ
diff --git a/docs/redirects.ts b/docs/redirects.ts
new file mode 100644
index 0000000000..ea909c829f
--- /dev/null
+++ b/docs/redirects.ts
@@ -0,0 +1,255 @@
+import { NextConfig } from "next";
+
+export const redirects: NextConfig["redirects"] = () => [
+ {
+ source: "/docs/editor-api/converting-blocks",
+ destination: "/docs/features/interoperability",
+ permanent: true,
+ },
+ {
+ source: "/docs/ai/setup",
+ destination: "/docs/features/ai/getting-started",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/ariakit",
+ destination: "/docs/getting-started/ariakit",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/shadcn",
+ destination: "/docs/getting-started/shadcn",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/paste-handling",
+ destination: "/docs/features/interoperability",
+ permanent: true,
+ },
+ {
+ source: "/docs/ui-components/formatting-toolbar",
+ destination: "/docs/react/components/formatting-toolbar",
+ permanent: true,
+ },
+ {
+ source: "/docs/ui-components/suggestion-menus",
+ destination: "/docs/react/components/suggestion-menus",
+ permanent: true,
+ },
+ {
+ source: "/docs/ui-components/side-menu",
+ destination: "/docs/react/components/side-menu",
+ permanent: true,
+ },
+ {
+ source: "/docs/ui-components/link-toolbar",
+ destination: "/docs/react/components/link-toolbar",
+ permanent: true,
+ },
+ {
+ source: "/docs/custom-schemas/custom-blocks",
+ destination: "/docs/features/custom-schemas/custom-blocks",
+ permanent: true,
+ },
+ {
+ source: "/docs/custom-schemas/custom-inline-content",
+ destination: "/docs/features/custom-schemas/custom-inline-content",
+ permanent: true,
+ },
+ {
+ source: "/docs/custom-schemas/custom-styles",
+ destination: "/docs/features/custom-schemas/custom-styles",
+ permanent: true,
+ },
+ {
+ source: "/docs/custom-schemas",
+ destination: "/docs/features/custom-schemas",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-basics/document-structure",
+ destination: "/docs/foundations/document-structure",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-basics/default-schema",
+ destination: "/docs/foundations/schemas",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-api/manipulating-blocks",
+ destination: "/docs/reference/editor/manipulating-content",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-api/manipulating-inline-content",
+ destination: "/docs/reference/editor/manipulating-content",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-api/cursor-selections",
+ destination: "/docs/reference/editor/cursor-selections",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/code-blocks",
+ destination: "/docs/features/blocks/code-blocks",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/tables",
+ destination: "/docs/features/blocks/tables",
+ permanent: true,
+ },
+ {
+ source: "/docs/ai/custom-commands",
+ destination: "/docs/features/ai/custom-commands",
+ permanent: true,
+ },
+ {
+ source: "/docs/ai/getting-started",
+ destination: "/docs/features/ai/getting-started",
+ permanent: true,
+ },
+ {
+ source: "/docs/ai/reference",
+ destination: "/docs/features/ai/reference",
+ permanent: true,
+ },
+ {
+ source: "/docs/ai",
+ destination: "/docs/features/ai",
+ permanent: true,
+ },
+ {
+ source: "/docs/styling-theming/overriding-css",
+ destination: "/docs/react/styling-theming/overriding-css",
+ permanent: true,
+ },
+ {
+ source: "/docs/styling-theming/themes",
+ destination: "/docs/react/styling-theming/themes",
+ permanent: true,
+ },
+ {
+ source: "/docs/styling-theming/adding-dom-attributes",
+ destination: "/docs/react/styling-theming/adding-dom-attributes",
+ permanent: true,
+ },
+ {
+ source: "/docs/collaboration/real-time-collaboration",
+ destination: "/docs/features/collaboration",
+ permanent: true,
+ },
+ {
+ source: "/docs/collaboration/comments",
+ destination: "/docs/features/collaboration/comments",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-api/server-processing",
+ destination: "/docs/features/server-processing",
+ permanent: true,
+ },
+ { source: "/docs/introduction", destination: "/docs", permanent: true },
+ {
+ source: "/docs/quickstart",
+ destination: "/docs/install",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor",
+ destination: "/docs/getting-started",
+ permanent: true,
+ },
+ {
+ source: "/docs/theming",
+ destination: "/docs/react/styling-theming",
+ permanent: true,
+ },
+ {
+ source: "/docs/formatting-toolbar",
+ destination: "/docs/react/components/formatting-toolbar",
+ permanent: true,
+ },
+ {
+ source: "/docs/slash-menu",
+ destination: "/docs/react/components/suggestion-menus",
+ permanent: true,
+ },
+ {
+ source: "/docs/side-menu",
+ destination: "/docs/react/components/side-menu",
+ permanent: true,
+ },
+ {
+ source: "/docs/ui-elements",
+ destination: "/docs/react/components",
+ permanent: true,
+ },
+ {
+ source: "/docs/blocks",
+ destination: "/docs/foundations/document-structure",
+ permanent: true,
+ },
+ {
+ source: "/docs/block-types",
+ destination: "/docs/features/blocks",
+ permanent: true,
+ },
+ {
+ source: "/docs/editor-basics/setup",
+ destination: "/docs/getting-started/editor-setup",
+ permanent: true,
+ },
+ {
+ source: "/docs/manipulating-blocks",
+ destination: "/docs/reference/editor/manipulating-content",
+ permanent: true,
+ },
+ {
+ source: "/docs/inline-content",
+ destination: "/docs/reference/editor/manipulating-content",
+ permanent: true,
+ },
+ {
+ source: "/docs/cursor-selections",
+ destination: "/docs/reference/editor/cursor-selections",
+ permanent: true,
+ },
+ {
+ source: "/docs/converting-blocks",
+ destination: "/docs/foundations/supported-formats",
+ permanent: true,
+ },
+ {
+ source: "/docs/real-time-collaboration",
+ destination: "/docs/features/collaboration",
+ permanent: true,
+ },
+ {
+ source: "/docs/nextjs",
+ destination: "/docs/getting-started/nextjs",
+ permanent: true,
+ },
+ {
+ source: "/docs/vanilla-js",
+ destination: "/docs/getting-started/vanilla-js",
+ permanent: true,
+ },
+ {
+ source: "/docs/advanced/real-time-collaboration",
+ destination: "/docs/features/collaboration",
+ permanent: true,
+ },
+ {
+ source: "/privacy-policy",
+ destination: "/legal/privacy-policy",
+ permanent: true,
+ },
+ {
+ source: "/terms-and-conditions",
+ destination: "/legal/terms-and-conditions",
+ permanent: true,
+ },
+];
diff --git a/docs/sentry.edge.config.ts b/docs/sentry.edge.config.ts
new file mode 100644
index 0000000000..752fe1d5b3
--- /dev/null
+++ b/docs/sentry.edge.config.ts
@@ -0,0 +1,23 @@
+// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
+// The config you add here will be used whenever one of the edge features is loaded.
+// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: "https://31af815601e4174f4443c863953eebe7@o4508925169500160.ingest.de.sentry.io/4508925646078032",
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // Enable sending user PII (Personally Identifiable Information)
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
+ sendDefaultPii: false,
+});
diff --git a/docs/sentry.server.config.ts b/docs/sentry.server.config.ts
new file mode 100644
index 0000000000..f324f532ce
--- /dev/null
+++ b/docs/sentry.server.config.ts
@@ -0,0 +1,22 @@
+// This file configures the initialization of Sentry on the server.
+// The config you add here will be used whenever the server handles a request.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: "https://31af815601e4174f4443c863953eebe7@o4508925169500160.ingest.de.sentry.io/4508925646078032",
+
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // Enable sending user PII (Personally Identifiable Information)
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
+ sendDefaultPii: false,
+});
diff --git a/docs/source.config.ts b/docs/source.config.ts
new file mode 100644
index 0000000000..b67079e70b
--- /dev/null
+++ b/docs/source.config.ts
@@ -0,0 +1,82 @@
+import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
+import {
+ defineConfig,
+ defineDocs,
+ frontmatterSchema,
+ metaSchema,
+} from "fumadocs-mdx/config";
+import { transformerTwoslash } from "fumadocs-twoslash";
+import { createFileSystemTypesCache } from "fumadocs-twoslash/cache-fs";
+import { z } from "zod/v4";
+
+// You can customise Zod schemas for frontmatter and `meta.json` here
+// see https://fumadocs.dev/docs/mdx/collections
+export const docs = defineDocs({
+ dir: "content/docs",
+ docs: {
+ schema: frontmatterSchema.extend({
+ // description: z.string(), // make required (unfortunately, breaks build)
+ imageTitle: z.string().optional(), // add imageTitle to customize text on og image
+ }),
+ postprocess: {
+ includeProcessedMarkdown: true,
+ },
+ },
+ meta: {
+ schema: metaSchema,
+ },
+});
+
+export const pages = defineDocs({
+ dir: "content/pages",
+ docs: {
+ schema: frontmatterSchema.extend({
+ // description: z.string(), // make required (unfortunately, breaks build)
+ imageTitle: z.string().optional(), // add imageTitle to customize text on og image
+ }),
+ postprocess: {
+ includeProcessedMarkdown: true,
+ },
+ },
+ meta: {
+ schema: metaSchema,
+ },
+});
+
+export const examples = defineDocs({
+ dir: "content/examples",
+ docs: {
+ schema: frontmatterSchema.extend({
+ author: z.string().optional(),
+ isPro: z.boolean().optional(),
+ imageTitle: z.string().optional(), // add imageTitle to customize text on og image
+ }),
+ postprocess: {
+ includeProcessedMarkdown: true,
+ },
+ },
+
+ meta: {
+ schema: metaSchema,
+ },
+});
+
+export default defineConfig({
+ mdxOptions: {
+ rehypeCodeOptions: {
+ themes: {
+ light: "github-light",
+ dark: "github-dark",
+ },
+ transformers: [
+ ...(rehypeCodeDefaultOptions.transformers ?? []),
+ transformerTwoslash({
+ typesCache: createFileSystemTypesCache(),
+ }),
+ ],
+ // important: Shiki doesn't support lazy loading languages for codeblocks in Twoslash popups
+ // make sure to define them first (e.g. the common ones)
+ langs: ["js", "jsx", "ts", "tsx", "css"],
+ },
+ },
+});
diff --git a/docs/sqlite.db b/docs/sqlite.db
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 0000000000..3838223ce4
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "target": "ESNext",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "paths": {
+ "@/*": ["./*"],
+ "fumadocs-mdx:collections/*": [".source/*"]
+ },
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/docs/validate-links.mjs b/docs/validate-links.mjs
new file mode 100644
index 0000000000..d9a45f69b2
--- /dev/null
+++ b/docs/validate-links.mjs
@@ -0,0 +1,50 @@
+import { getTableOfContents } from "fumadocs-core/content/toc";
+import { getSlugs } from "fumadocs-core/source";
+import {
+ printErrors,
+ readFiles,
+ scanURLs,
+ validateFiles,
+} from "next-validate-link";
+import path from "node:path";
+async function checkLinks() {
+ const docsFiles = await readFiles("content/docs/**/*.{md,mdx}");
+ const pagesFiles = await readFiles("content/pages/**/*.{md,mdx}");
+ const examplesFiles = await readFiles("content/examples/**/*.{md,mdx}");
+
+ const scanned = await scanURLs({
+ populate: {
+ "[...slug]": pagesFiles.map((file) => {
+ return {
+ value: getSlugs(path.relative("content/pages", file.path)),
+ hashes: getTableOfContents(file.content).map((item) =>
+ item.url.slice(1),
+ ),
+ };
+ }),
+ "docs/[[...slug]]": docsFiles.map((file) => {
+ return {
+ value: getSlugs(path.relative("content/docs", file.path)),
+ hashes: getTableOfContents(file.content).map((item) =>
+ item.url.slice(1),
+ ),
+ };
+ }),
+ "examples/[[...slug]]": examplesFiles.map((file) => {
+ return {
+ value: getSlugs(path.relative("content/examples", file.path)),
+ hashes: getTableOfContents(file.content).map((item) =>
+ item.url.slice(1),
+ ),
+ };
+ }),
+ },
+ });
+ printErrors(
+ await validateFiles([...docsFiles, ...pagesFiles, ...examplesFiles], {
+ scanned,
+ }),
+ true,
+ );
+}
+void checkLinks();
diff --git a/docs/vercel.json b/docs/vercel.json
new file mode 100644
index 0000000000..bd6fd8faa8
--- /dev/null
+++ b/docs/vercel.json
@@ -0,0 +1,3 @@
+{
+ "cleanUrls": true
+}
diff --git a/examples/.eslintrc.js b/examples/.eslintrc.js
new file mode 100644
index 0000000000..c78910b8b6
--- /dev/null
+++ b/examples/.eslintrc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ extends: ["../.eslintrc.js"],
+ rules: {
+ "import/extensions": "off",
+ },
+};
diff --git a/examples/01-basic/01-minimal/.bnexample.json b/examples/01-basic/01-minimal/.bnexample.json
new file mode 100644
index 0000000000..6d4a02dd52
--- /dev/null
+++ b/examples/01-basic/01-minimal/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/01-minimal/README.md b/examples/01-basic/01-minimal/README.md
new file mode 100644
index 0000000000..547eb58a2e
--- /dev/null
+++ b/examples/01-basic/01-minimal/README.md
@@ -0,0 +1,7 @@
+# Basic Setup
+
+This example shows the minimal code required to set up a BlockNote editor in React.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/01-minimal/index.html b/examples/01-basic/01-minimal/index.html
new file mode 100644
index 0000000000..7f8240617e
--- /dev/null
+++ b/examples/01-basic/01-minimal/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Basic Setup
+
+
+
+
+
+
+
diff --git a/examples/01-basic/01-minimal/main.tsx b/examples/01-basic/01-minimal/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/01-minimal/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/01-minimal/package.json b/examples/01-basic/01-minimal/package.json
new file mode 100644
index 0000000000..26f63572f5
--- /dev/null
+++ b/examples/01-basic/01-minimal/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-minimal",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/01-minimal/src/App.tsx b/examples/01-basic/01-minimal/src/App.tsx
new file mode 100644
index 0000000000..a3b92bafd2
--- /dev/null
+++ b/examples/01-basic/01-minimal/src/App.tsx
@@ -0,0 +1,12 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/01-minimal/tsconfig.json b/examples/01-basic/01-minimal/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/01-minimal/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/01-minimal/vite.config.ts b/examples/01-basic/01-minimal/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/01-minimal/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/02-block-objects/.bnexample.json b/examples/01-basic/02-block-objects/.bnexample.json
new file mode 100644
index 0000000000..6b15063ef2
--- /dev/null
+++ b/examples/01-basic/02-block-objects/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Basic", "Blocks", "Inline Content"]
+}
diff --git a/examples/01-basic/02-block-objects/README.md b/examples/01-basic/02-block-objects/README.md
new file mode 100644
index 0000000000..18e9120493
--- /dev/null
+++ b/examples/01-basic/02-block-objects/README.md
@@ -0,0 +1,10 @@
+# Displaying Document JSON
+
+In this example, the document's JSON representation is displayed below the editor.
+
+**Try it out:** Try typing in the editor and see the JSON update!
+
+**Relevant Docs:**
+
+- [Document Structure](/docs/foundations/document-structure)
+- [Getting the Document](/docs/reference/editor/manipulating-content)
diff --git a/examples/01-basic/02-block-objects/index.html b/examples/01-basic/02-block-objects/index.html
new file mode 100644
index 0000000000..7dbba2a212
--- /dev/null
+++ b/examples/01-basic/02-block-objects/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Displaying Document JSON
+
+
+
+
+
+
+
diff --git a/examples/01-basic/02-block-objects/main.tsx b/examples/01-basic/02-block-objects/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/02-block-objects/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/02-block-objects/package.json b/examples/01-basic/02-block-objects/package.json
new file mode 100644
index 0000000000..908df7ca16
--- /dev/null
+++ b/examples/01-basic/02-block-objects/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-block-objects",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/02-block-objects/src/App.tsx b/examples/01-basic/02-block-objects/src/App.tsx
new file mode 100644
index 0000000000..c3d623f2e8
--- /dev/null
+++ b/examples/01-basic/02-block-objects/src/App.tsx
@@ -0,0 +1,56 @@
+import { Block } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { useEffect, useState } from "react";
+
+import "./styles.css";
+
+export default function App() {
+ // Stores the document JSON.
+ const [blocks, setBlocks] = useState([]);
+
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "heading",
+ content: "This is a heading block",
+ },
+ {
+ type: "paragraph",
+ content: "This is a paragraph block",
+ },
+ ],
+ });
+
+ // Sets the initial document JSON
+ useEffect(() => setBlocks(editor.document), []);
+
+ // Renders the editor instance and its document JSON.
+ return (
+
+
BlockNote Editor:
+
+ {
+ // Sets the document JSON whenever the editor content changes.
+ setBlocks(editor.document);
+ }}
+ />
+
+
Document JSON:
+
+
+ {JSON.stringify(blocks, null, 2)}
+
+
+
+ );
+}
diff --git a/examples/01-basic/02-block-objects/src/styles.css b/examples/01-basic/02-block-objects/src/styles.css
new file mode 100644
index 0000000000..6d5eeba7fe
--- /dev/null
+++ b/examples/01-basic/02-block-objects/src/styles.css
@@ -0,0 +1,25 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.item {
+ border-radius: 0.5rem;
+ flex: 1;
+ overflow: hidden;
+}
+
+.item.bordered {
+ border: 1px solid gray;
+}
+
+.item pre {
+ border-radius: 0.5rem;
+ height: 100%;
+ overflow: auto;
+ padding-block: 1rem;
+ padding-inline: 54px;
+ width: 100%;
+ white-space: pre-wrap;
+}
diff --git a/examples/01-basic/02-block-objects/tsconfig.json b/examples/01-basic/02-block-objects/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/02-block-objects/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/02-block-objects/vite.config.ts b/examples/01-basic/02-block-objects/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/02-block-objects/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/03-multi-column/.bnexample.json b/examples/01-basic/03-multi-column/.bnexample.json
new file mode 100644
index 0000000000..19dc65d063
--- /dev/null
+++ b/examples/01-basic/03-multi-column/.bnexample.json
@@ -0,0 +1,10 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Basic", "Blocks"],
+ "dependencies": {
+ "@blocknote/xl-multi-column": "latest"
+ },
+ "pro": true
+}
diff --git a/examples/01-basic/03-multi-column/README.md b/examples/01-basic/03-multi-column/README.md
new file mode 100644
index 0000000000..6c2e663d21
--- /dev/null
+++ b/examples/01-basic/03-multi-column/README.md
@@ -0,0 +1,8 @@
+# Multi-Column Blocks
+
+This example showcases multi-column blocks, allowing you to stack blocks next to each other. These come as part of the `@blocknote/xl-multi-column` package.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Document Structure](/docs/foundations/document-structure)
diff --git a/examples/01-basic/03-multi-column/index.html b/examples/01-basic/03-multi-column/index.html
new file mode 100644
index 0000000000..914212d28f
--- /dev/null
+++ b/examples/01-basic/03-multi-column/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Multi-Column Blocks
+
+
+
+
+
+
+
diff --git a/examples/01-basic/03-multi-column/main.tsx b/examples/01-basic/03-multi-column/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/03-multi-column/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/03-multi-column/package.json b/examples/01-basic/03-multi-column/package.json
new file mode 100644
index 0000000000..2ce018ce9c
--- /dev/null
+++ b/examples/01-basic/03-multi-column/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-basic-multi-column",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-multi-column": "latest"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/03-multi-column/src/App.tsx b/examples/01-basic/03-multi-column/src/App.tsx
new file mode 100644
index 0000000000..c688406214
--- /dev/null
+++ b/examples/01-basic/03-multi-column/src/App.tsx
@@ -0,0 +1,119 @@
+import {
+ BlockNoteSchema,
+ combineByGroup,
+} from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import * as locales from "@blocknote/core/locales";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ getMultiColumnSlashMenuItems,
+ multiColumnDropCursor,
+ locales as multiColumnLocales,
+ withMultiColumn,
+} from "@blocknote/xl-multi-column";
+import { useMemo } from "react";
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ // Adds column and column list blocks to the schema.
+ schema: withMultiColumn(BlockNoteSchema.create()),
+ // The default drop cursor only shows up above and below blocks - we replace
+ // it with the multi-column one that also shows up on the sides of blocks.
+ dropCursor: multiColumnDropCursor,
+ // Merges the default dictionary with the multi-column dictionary.
+ dictionary: {
+ ...locales.en,
+ multi_column: multiColumnLocales.en,
+ },
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "columnList",
+ children: [
+ {
+ type: "column",
+ props: {
+ width: 0.8,
+ },
+ children: [
+ {
+ type: "paragraph",
+ content: "This paragraph is in a column!",
+ },
+ ],
+ },
+ {
+ type: "column",
+ props: {
+ width: 1.4,
+ },
+ children: [
+ {
+ type: "heading",
+ content: "So is this heading!",
+ },
+ ],
+ },
+ {
+ type: "column",
+ props: {
+ width: 0.8,
+ },
+ children: [
+ {
+ type: "paragraph",
+ content: "You can have multiple blocks in a column too",
+ },
+ {
+ type: "bulletListItem",
+ content: "Block 1",
+ },
+ {
+ type: "bulletListItem",
+ content: "Block 2",
+ },
+ {
+ type: "bulletListItem",
+ content: "Block 3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ // Gets the default slash menu items merged with the multi-column ones.
+ const getSlashMenuItems = useMemo(() => {
+ return async (query: string) =>
+ filterSuggestionItems(
+ combineByGroup(
+ getDefaultReactSlashMenuItems(editor),
+ getMultiColumnSlashMenuItems(editor),
+ ),
+ query,
+ );
+ }, [editor]);
+
+ // Renders the editor instance using a React component.
+ return (
+
+ {/* Replaces the default slash menu with one that has both the default
+ items and the multi-column ones. */}
+
+
+ );
+}
diff --git a/examples/01-basic/03-multi-column/tsconfig.json b/examples/01-basic/03-multi-column/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/03-multi-column/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/03-multi-column/vite.config.ts b/examples/01-basic/03-multi-column/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/03-multi-column/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/04-default-blocks/.bnexample.json b/examples/01-basic/04-default-blocks/.bnexample.json
new file mode 100644
index 0000000000..6b15063ef2
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Basic", "Blocks", "Inline Content"]
+}
diff --git a/examples/01-basic/04-default-blocks/README.md b/examples/01-basic/04-default-blocks/README.md
new file mode 100644
index 0000000000..2bb4b609f7
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/README.md
@@ -0,0 +1,9 @@
+# Default Schema Showcase
+
+This example showcases each block and inline content type in BlockNote's default schema.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Document Structure](/docs/foundations/document-structure)
+- [Default Schema](/docs/foundations/schemas)
diff --git a/examples/01-basic/04-default-blocks/index.html b/examples/01-basic/04-default-blocks/index.html
new file mode 100644
index 0000000000..e3e4e92661
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Default Schema Showcase
+
+
+
+
+
+
+
diff --git a/examples/01-basic/04-default-blocks/main.tsx b/examples/01-basic/04-default-blocks/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/04-default-blocks/package.json b/examples/01-basic/04-default-blocks/package.json
new file mode 100644
index 0000000000..8546777d34
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-default-blocks",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/04-default-blocks/src/App.tsx b/examples/01-basic/04-default-blocks/src/App.tsx
new file mode 100644
index 0000000000..0d55d1af3d
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/src/App.tsx
@@ -0,0 +1,153 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Blocks:",
+ styles: { bold: true },
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ },
+ {
+ id: "toggle-heading",
+ type: "heading",
+ props: { isToggleable: true },
+ content: "Toggle Heading",
+ },
+ {
+ type: "quote",
+ content: "Quote",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ },
+ {
+ id: "toggle-list-item",
+ type: "toggleListItem",
+ content: "Toggle List Item",
+ },
+ {
+ type: "codeBlock",
+ props: { language: "javascript" },
+ content: "console.log('Hello, world!');",
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ ],
+ },
+ },
+ {
+ type: "file",
+ },
+ {
+ type: "image",
+ props: {
+ url: "https://placehold.co/332x322.jpg",
+ caption: "From https://placehold.co/332x322.jpg",
+ },
+ },
+ {
+ type: "video",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ },
+ {
+ type: "audio",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ },
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Inline Content:",
+ styles: { bold: true },
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Styled Text",
+ styles: {
+ bold: true,
+ italic: true,
+ textColor: "red",
+ backgroundColor: "blue",
+ },
+ },
+ {
+ type: "text",
+ text: " ",
+ styles: {},
+ },
+ {
+ type: "link",
+ content: "Link",
+ href: "https://www.blocknotejs.org",
+ },
+ ],
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/04-default-blocks/tsconfig.json b/examples/01-basic/04-default-blocks/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/04-default-blocks/vite.config.ts b/examples/01-basic/04-default-blocks/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/04-default-blocks/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/05-removing-default-blocks/.bnexample.json b/examples/01-basic/05-removing-default-blocks/.bnexample.json
new file mode 100644
index 0000000000..414e193313
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "hunxjunedo",
+ "tags": ["Basic", "removing", "blocks"]
+}
diff --git a/examples/01-basic/05-removing-default-blocks/README.md b/examples/01-basic/05-removing-default-blocks/README.md
new file mode 100644
index 0000000000..f950e2ad31
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/README.md
@@ -0,0 +1,9 @@
+# Removing Default Blocks from Schema
+
+This example shows how to change the default schema and disable the Audio and Image blocks. To do this, we pass in a custom schema based on the built-in, default schema, with two specific blocks removed.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Custom Schemas](/docs/features/custom-schemas)
+- [Default Schema](/docs/foundations/schemas)
diff --git a/examples/01-basic/05-removing-default-blocks/index.html b/examples/01-basic/05-removing-default-blocks/index.html
new file mode 100644
index 0000000000..f95160e28c
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Removing Default Blocks from Schema
+
+
+
+
+
+
+
diff --git a/examples/01-basic/05-removing-default-blocks/main.tsx b/examples/01-basic/05-removing-default-blocks/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/05-removing-default-blocks/package.json b/examples/01-basic/05-removing-default-blocks/package.json
new file mode 100644
index 0000000000..72c8e366f4
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-removing-default-blocks",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/05-removing-default-blocks/src/App.tsx b/examples/01-basic/05-removing-default-blocks/src/App.tsx
new file mode 100644
index 0000000000..d56523af3a
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/src/App.tsx
@@ -0,0 +1,26 @@
+import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Disable the Audio and Image blocks from the built-in schema
+ // This is done by picking out the blocks you want to disable
+ const { audio, image, ...remainingBlockSpecs } = defaultBlockSpecs;
+
+ const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ // remainingBlockSpecs contains all the other blocks
+ ...remainingBlockSpecs,
+ },
+ });
+
+ // Creates a new editor instance with the schema
+ const editor = useCreateBlockNote({
+ schema,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/05-removing-default-blocks/tsconfig.json b/examples/01-basic/05-removing-default-blocks/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/05-removing-default-blocks/vite.config.ts b/examples/01-basic/05-removing-default-blocks/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/05-removing-default-blocks/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/06-block-manipulation/.bnexample.json b/examples/01-basic/06-block-manipulation/.bnexample.json
new file mode 100644
index 0000000000..c77e72a9b9
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic", "Blocks"]
+}
diff --git a/examples/01-basic/06-block-manipulation/README.md b/examples/01-basic/06-block-manipulation/README.md
new file mode 100644
index 0000000000..b09de35dfd
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/README.md
@@ -0,0 +1,7 @@
+# Manipulating Blocks
+
+This example shows 4 buttons to manipulate the first block using the `insertBlocks`, `updateBlock`, `removeBlocks` and `replaceBlocks` methods.
+
+**Relevant Docs:**
+
+- [Block Manipulation](/docs/reference/editor/manipulating-content)
diff --git a/examples/01-basic/06-block-manipulation/index.html b/examples/01-basic/06-block-manipulation/index.html
new file mode 100644
index 0000000000..7bdf77e285
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Manipulating Blocks
+
+
+
+
+
+
+
diff --git a/examples/01-basic/06-block-manipulation/main.tsx b/examples/01-basic/06-block-manipulation/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/06-block-manipulation/package.json b/examples/01-basic/06-block-manipulation/package.json
new file mode 100644
index 0000000000..9f4c9b0764
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-block-manipulation",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/06-block-manipulation/src/App.tsx b/examples/01-basic/06-block-manipulation/src/App.tsx
new file mode 100644
index 0000000000..b92008ef72
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/src/App.tsx
@@ -0,0 +1,74 @@
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import "./styles.css";
+
+export default function App() {
+ const editor = useCreateBlockNote();
+
+ return (
+
+
+
+ {/*Inserts a new block at start of document.*/}
+
+ {/*Updates the first block*/}
+
+ {/*Removes the first block*/}
+
+ {/*Replaces the first block*/}
+
+
+
+ );
+}
diff --git a/examples/01-basic/06-block-manipulation/src/styles.css b/examples/01-basic/06-block-manipulation/src/styles.css
new file mode 100644
index 0000000000..cc97b34a4f
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/src/styles.css
@@ -0,0 +1,15 @@
+.edit-buttons {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+
+.edit-button {
+ border: 1px solid gray;
+ border-radius: 4px;
+ padding-inline: 4px;
+}
+
+.edit-button:hover {
+ border: 1px solid lightgrey;
+}
diff --git a/examples/01-basic/06-block-manipulation/tsconfig.json b/examples/01-basic/06-block-manipulation/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/06-block-manipulation/vite.config.ts b/examples/01-basic/06-block-manipulation/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/06-block-manipulation/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/07-selection-blocks/.bnexample.json b/examples/01-basic/07-selection-blocks/.bnexample.json
new file mode 100644
index 0000000000..c77e72a9b9
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic", "Blocks"]
+}
diff --git a/examples/01-basic/07-selection-blocks/README.md b/examples/01-basic/07-selection-blocks/README.md
new file mode 100644
index 0000000000..a8d689c24b
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/README.md
@@ -0,0 +1,9 @@
+# Displaying Selected Blocks
+
+In this example, the JSON representation of blocks spanned by the user's selection, is displayed below the editor.
+
+**Try it out:** Select different blocks in the editor and see the JSON update!
+
+**Relevant Docs:**
+
+- [Cursor Selections](/docs/reference/editor/cursor-selections)
diff --git a/examples/01-basic/07-selection-blocks/index.html b/examples/01-basic/07-selection-blocks/index.html
new file mode 100644
index 0000000000..a51458b513
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Displaying Selected Blocks
+
+
+
+
+
+
+
diff --git a/examples/01-basic/07-selection-blocks/main.tsx b/examples/01-basic/07-selection-blocks/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/07-selection-blocks/package.json b/examples/01-basic/07-selection-blocks/package.json
new file mode 100644
index 0000000000..13106a8e6d
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-selection-blocks",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/07-selection-blocks/src/App.tsx b/examples/01-basic/07-selection-blocks/src/App.tsx
new file mode 100644
index 0000000000..811b14e328
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/src/App.tsx
@@ -0,0 +1,56 @@
+import { Block } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useState } from "react";
+
+import "./styles.css";
+
+export default function App() {
+ // Stores the selected blocks as an array of Block objects.
+ const [blocks, setBlocks] = useState([]);
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Select different blocks to see the JSON change below",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
BlockNote Editor:
+
+ {
+ const selection = editor.getSelection();
+
+ // Get the blocks in the current selection and store on the state. If
+ // the selection is empty, store the block containing the text cursor
+ // instead.
+ if (selection !== undefined) {
+ setBlocks(selection.blocks);
+ } else {
+ setBlocks([editor.getTextCursorPosition().block]);
+ }
+ }}
+ />
+
+
Selection JSON:
+
+
+ {JSON.stringify(blocks, null, 2)}
+
+
+
+ );
+}
diff --git a/examples/01-basic/07-selection-blocks/src/styles.css b/examples/01-basic/07-selection-blocks/src/styles.css
new file mode 100644
index 0000000000..6d5eeba7fe
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/src/styles.css
@@ -0,0 +1,25 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.item {
+ border-radius: 0.5rem;
+ flex: 1;
+ overflow: hidden;
+}
+
+.item.bordered {
+ border: 1px solid gray;
+}
+
+.item pre {
+ border-radius: 0.5rem;
+ height: 100%;
+ overflow: auto;
+ padding-block: 1rem;
+ padding-inline: 54px;
+ width: 100%;
+ white-space: pre-wrap;
+}
diff --git a/examples/01-basic/07-selection-blocks/tsconfig.json b/examples/01-basic/07-selection-blocks/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/07-selection-blocks/vite.config.ts b/examples/01-basic/07-selection-blocks/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/07-selection-blocks/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/08-ariakit/.bnexample.json b/examples/01-basic/08-ariakit/.bnexample.json
new file mode 100644
index 0000000000..6d4a02dd52
--- /dev/null
+++ b/examples/01-basic/08-ariakit/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/08-ariakit/README.md b/examples/01-basic/08-ariakit/README.md
new file mode 100644
index 0000000000..3eae9a9a01
--- /dev/null
+++ b/examples/01-basic/08-ariakit/README.md
@@ -0,0 +1,8 @@
+# Use with Ariakit
+
+This example shows how you can use BlockNote with Ariakit (instead of Mantine).
+
+**Relevant Docs:**
+
+- [Ariakit Docs](/docs/getting-started/ariakit)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/08-ariakit/index.html b/examples/01-basic/08-ariakit/index.html
new file mode 100644
index 0000000000..e4594a8ae2
--- /dev/null
+++ b/examples/01-basic/08-ariakit/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Use with Ariakit
+
+
+
+
+
+
+
diff --git a/examples/01-basic/08-ariakit/main.tsx b/examples/01-basic/08-ariakit/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/08-ariakit/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/08-ariakit/package.json b/examples/01-basic/08-ariakit/package.json
new file mode 100644
index 0000000000..2e9ff90086
--- /dev/null
+++ b/examples/01-basic/08-ariakit/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-ariakit",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/08-ariakit/src/App.tsx b/examples/01-basic/08-ariakit/src/App.tsx
new file mode 100644
index 0000000000..1a86f6b436
--- /dev/null
+++ b/examples/01-basic/08-ariakit/src/App.tsx
@@ -0,0 +1,12 @@
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/ariakit";
+import "@blocknote/ariakit/style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/08-ariakit/tsconfig.json b/examples/01-basic/08-ariakit/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/08-ariakit/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/08-ariakit/vite.config.ts b/examples/01-basic/08-ariakit/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/08-ariakit/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/09-shadcn/.bnexample.json b/examples/01-basic/09-shadcn/.bnexample.json
new file mode 100644
index 0000000000..baf1eb6306
--- /dev/null
+++ b/examples/01-basic/09-shadcn/.bnexample.json
@@ -0,0 +1,8 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic"],
+ "tailwind": true,
+ "stackBlitz": false
+}
diff --git a/examples/01-basic/09-shadcn/README.md b/examples/01-basic/09-shadcn/README.md
new file mode 100644
index 0000000000..e3030d4929
--- /dev/null
+++ b/examples/01-basic/09-shadcn/README.md
@@ -0,0 +1,7 @@
+# Use with ShadCN
+
+This example shows how you can use BlockNote with ShadCN (instead of Mantine).
+
+**Relevant Docs:**
+
+- [Getting Started with ShadCN](/docs/getting-started/shadcn)
diff --git a/examples/01-basic/09-shadcn/index.html b/examples/01-basic/09-shadcn/index.html
new file mode 100644
index 0000000000..84a8096813
--- /dev/null
+++ b/examples/01-basic/09-shadcn/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Use with ShadCN
+
+
+
+
+
+
+
diff --git a/examples/01-basic/09-shadcn/main.tsx b/examples/01-basic/09-shadcn/main.tsx
new file mode 100644
index 0000000000..a4000743b6
--- /dev/null
+++ b/examples/01-basic/09-shadcn/main.tsx
@@ -0,0 +1,12 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+import "./tailwind.css";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/09-shadcn/package.json b/examples/01-basic/09-shadcn/package.json
new file mode 100644
index 0000000000..970aee759f
--- /dev/null
+++ b/examples/01-basic/09-shadcn/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@blocknote/example-basic-shadcn",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "tailwindcss": "^4.1.14",
+ "tw-animate-css": "^1.4.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.14",
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/09-shadcn/src/App.tsx b/examples/01-basic/09-shadcn/src/App.tsx
new file mode 100644
index 0000000000..754b4a5e94
--- /dev/null
+++ b/examples/01-basic/09-shadcn/src/App.tsx
@@ -0,0 +1,22 @@
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/shadcn";
+import "@blocknote/shadcn/style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return (
+
+ );
+}
diff --git a/examples/01-basic/09-shadcn/tailwind.css b/examples/01-basic/09-shadcn/tailwind.css
new file mode 100644
index 0000000000..eb17734f35
--- /dev/null
+++ b/examples/01-basic/09-shadcn/tailwind.css
@@ -0,0 +1,121 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+/* Code below needed for ShadCN examples, check docs for more info. */
+@source "./node_modules/@blocknote/shadcn";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ .bn-shadcn * {
+ @apply border-border outline-ring/50;
+ }
+}
diff --git a/examples/01-basic/09-shadcn/tsconfig.json b/examples/01-basic/09-shadcn/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/09-shadcn/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/09-shadcn/vite.config.ts b/examples/01-basic/09-shadcn/vite.config.ts
new file mode 100644
index 0000000000..3de2df11ea
--- /dev/null
+++ b/examples/01-basic/09-shadcn/vite.config.ts
@@ -0,0 +1,33 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+import tailwindcss from "@tailwindcss/vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react(), tailwindcss()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/10-localization/.bnexample.json b/examples/01-basic/10-localization/.bnexample.json
new file mode 100644
index 0000000000..fd06fde1e0
--- /dev/null
+++ b/examples/01-basic/10-localization/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/10-localization/README.md b/examples/01-basic/10-localization/README.md
new file mode 100644
index 0000000000..9dc27ec369
--- /dev/null
+++ b/examples/01-basic/10-localization/README.md
@@ -0,0 +1,9 @@
+# Localization (i18n)
+
+In this example, we pass in a custom dictionary to change the interface of the editor to use Dutch (NL) strings.
+
+You can also provide your own dictionary to customize the strings used in the editor, or submit a Pull Request to add support for your language of your choice.
+
+**Relevant Docs:**
+
+- [Localization](/docs/features/localization)
diff --git a/examples/01-basic/10-localization/index.html b/examples/01-basic/10-localization/index.html
new file mode 100644
index 0000000000..efa7f80ad3
--- /dev/null
+++ b/examples/01-basic/10-localization/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Localization (i18n)
+
+
+
+
+
+
+
diff --git a/examples/01-basic/10-localization/main.tsx b/examples/01-basic/10-localization/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/10-localization/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/10-localization/package.json b/examples/01-basic/10-localization/package.json
new file mode 100644
index 0000000000..5431f4de04
--- /dev/null
+++ b/examples/01-basic/10-localization/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-localization",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/10-localization/src/App.tsx b/examples/01-basic/10-localization/src/App.tsx
new file mode 100644
index 0000000000..7000c3e436
--- /dev/null
+++ b/examples/01-basic/10-localization/src/App.tsx
@@ -0,0 +1,21 @@
+import { nl } from "@blocknote/core/locales";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+// import { useTranslation } from "some-i18n-library"; // You can use any i18n library you like
+
+export default function App() {
+ // const { lang } = useTranslation('common'); // You can get the current language from the i18n library or alternatively from a router
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ // Passes the Dutch (NL) dictionary to the editor instance.
+ // You can also provide your own dictionary here to customize the strings used in the editor,
+ // or submit a Pull Request to add support for your language of your choice
+ dictionary: nl,
+ // dictionary: locales[lang as keyof typeof locales], // Use the language from the i18n library dynamically
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/10-localization/tsconfig.json b/examples/01-basic/10-localization/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/10-localization/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/10-localization/vite.config.ts b/examples/01-basic/10-localization/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/10-localization/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/11-custom-placeholder/.bnexample.json b/examples/01-basic/11-custom-placeholder/.bnexample.json
new file mode 100644
index 0000000000..93dd5d3de3
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "ezhil56x",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/11-custom-placeholder/README.md b/examples/01-basic/11-custom-placeholder/README.md
new file mode 100644
index 0000000000..db435d7eee
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/README.md
@@ -0,0 +1,12 @@
+# Change placeholder text
+
+In this example, we show how to change the placeholders:
+
+- For an empty document, we show a placeholder `Start typing..` (by default, this is not set)
+- the default placeholder in this editor shows `Custom default placeholder` instead of the default (`Enter text or type '/' for commands`)
+- for Headings, the placeholder shows `Custom heading placeholder` instead of the default (`Heading`). Try adding a Heading to see the change
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Localization (i18n)](/examples/basic/localization)
diff --git a/examples/01-basic/11-custom-placeholder/index.html b/examples/01-basic/11-custom-placeholder/index.html
new file mode 100644
index 0000000000..24c535465c
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Change placeholder text
+
+
+
+
+
+
+
diff --git a/examples/01-basic/11-custom-placeholder/main.tsx b/examples/01-basic/11-custom-placeholder/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/11-custom-placeholder/package.json b/examples/01-basic/11-custom-placeholder/package.json
new file mode 100644
index 0000000000..b3ca292bfb
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-custom-placeholder",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/11-custom-placeholder/src/App.tsx b/examples/01-basic/11-custom-placeholder/src/App.tsx
new file mode 100644
index 0000000000..b1323aacb5
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/src/App.tsx
@@ -0,0 +1,30 @@
+import { en } from "@blocknote/core/locales";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // We use the English, default dictionary
+ const locale = en;
+
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ // We override the `placeholders` in our dictionary
+ dictionary: {
+ ...locale,
+ placeholders: {
+ ...locale.placeholders,
+ // We override the empty document placeholder
+ emptyDocument: "Start typing..",
+ // We override the default placeholder
+ default: "Custom default placeholder",
+ // We override the heading placeholder
+ heading: "Custom heading placeholder",
+ },
+ },
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/11-custom-placeholder/tsconfig.json b/examples/01-basic/11-custom-placeholder/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/11-custom-placeholder/vite.config.ts b/examples/01-basic/11-custom-placeholder/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/11-custom-placeholder/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/12-multi-editor/.bnexample.json b/examples/01-basic/12-multi-editor/.bnexample.json
new file mode 100644
index 0000000000..54cfd20571
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "areknawo",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/12-multi-editor/README.md b/examples/01-basic/12-multi-editor/README.md
new file mode 100644
index 0000000000..b2da731dd0
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/README.md
@@ -0,0 +1,7 @@
+# Multi-Editor Setup
+
+This example showcases use of multiple editors in a single page - you can even drag blocks between them.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/12-multi-editor/index.html b/examples/01-basic/12-multi-editor/index.html
new file mode 100644
index 0000000000..011941dc50
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Multi-Editor Setup
+
+
+
+
+
+
+
diff --git a/examples/01-basic/12-multi-editor/main.tsx b/examples/01-basic/12-multi-editor/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/12-multi-editor/package.json b/examples/01-basic/12-multi-editor/package.json
new file mode 100644
index 0000000000..2408f1d822
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-multi-editor",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/12-multi-editor/src/App.tsx b/examples/01-basic/12-multi-editor/src/App.tsx
new file mode 100644
index 0000000000..994e98830f
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/src/App.tsx
@@ -0,0 +1,55 @@
+import { PartialBlock } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+// Component that creates & renders a BlockNote editor.
+function Editor(props: {
+ initialContent?: PartialBlock[];
+ theme: "dark" | "light";
+}) {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: props.initialContent,
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+ );
+}
+
+export default function App() {
+ // Creates & renders two editors side by side.
+ return (
+
+
+
+
+ );
+}
diff --git a/examples/01-basic/12-multi-editor/tsconfig.json b/examples/01-basic/12-multi-editor/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/12-multi-editor/vite.config.ts b/examples/01-basic/12-multi-editor/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/12-multi-editor/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/13-custom-paste-handler/.bnexample.json b/examples/01-basic/13-custom-paste-handler/.bnexample.json
new file mode 100644
index 0000000000..5778b19ace
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "nperez0111",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/13-custom-paste-handler/README.md b/examples/01-basic/13-custom-paste-handler/README.md
new file mode 100644
index 0000000000..c19f9c2bbf
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/README.md
@@ -0,0 +1,9 @@
+# Custom Paste Handler
+
+In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.
+
+**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.
+
+**Relevant Docs:**
+
+- [Paste Handling](/docs/reference/editor/paste-handling)
diff --git a/examples/01-basic/13-custom-paste-handler/index.html b/examples/01-basic/13-custom-paste-handler/index.html
new file mode 100644
index 0000000000..b68dcceeba
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Custom Paste Handler
+
+
+
+
+
+
+
diff --git a/examples/01-basic/13-custom-paste-handler/main.tsx b/examples/01-basic/13-custom-paste-handler/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/13-custom-paste-handler/package.json b/examples/01-basic/13-custom-paste-handler/package.json
new file mode 100644
index 0000000000..8f2fd39c02
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-custom-paste-handler",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/13-custom-paste-handler/src/App.tsx b/examples/01-basic/13-custom-paste-handler/src/App.tsx
new file mode 100644
index 0000000000..a817ac4808
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/src/App.tsx
@@ -0,0 +1,109 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./styles.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: [
+ {
+ styles: {},
+ type: "text",
+ text: "Paste some text here",
+ },
+ ],
+ },
+ ],
+ pasteHandler: ({ event, editor, defaultPasteHandler }) => {
+ if (event.clipboardData?.types.includes("text/plain")) {
+ editor.pasteMarkdown(
+ event.clipboardData.getData("text/plain") +
+ " - inserted by the custom paste handler",
+ );
+ return true;
+ }
+ return defaultPasteHandler();
+ },
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/01-basic/13-custom-paste-handler/src/styles.css b/examples/01-basic/13-custom-paste-handler/src/styles.css
new file mode 100644
index 0000000000..cc97b34a4f
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/src/styles.css
@@ -0,0 +1,15 @@
+.edit-buttons {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+
+.edit-button {
+ border: 1px solid gray;
+ border-radius: 4px;
+ padding-inline: 4px;
+}
+
+.edit-button:hover {
+ border: 1px solid lightgrey;
+}
diff --git a/examples/01-basic/13-custom-paste-handler/tsconfig.json b/examples/01-basic/13-custom-paste-handler/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/13-custom-paste-handler/vite.config.ts b/examples/01-basic/13-custom-paste-handler/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/13-custom-paste-handler/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/14-editor-scrollable/.bnexample.json b/examples/01-basic/14-editor-scrollable/.bnexample.json
new file mode 100644
index 0000000000..e9c8bcb27b
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/14-editor-scrollable/README.md b/examples/01-basic/14-editor-scrollable/README.md
new file mode 100644
index 0000000000..0ae571ad1f
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/README.md
@@ -0,0 +1,7 @@
+# Scrollable Editor
+
+This example shows how to constrain the editor height and make it scrollable.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/14-editor-scrollable/index.html b/examples/01-basic/14-editor-scrollable/index.html
new file mode 100644
index 0000000000..54b1f87c4a
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Scrollable Editor
+
+
+
+
+
+
+
diff --git a/examples/01-basic/14-editor-scrollable/main.tsx b/examples/01-basic/14-editor-scrollable/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/14-editor-scrollable/package.json b/examples/01-basic/14-editor-scrollable/package.json
new file mode 100644
index 0000000000..395196edc9
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-editor-scrollable",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/14-editor-scrollable/src/App.tsx b/examples/01-basic/14-editor-scrollable/src/App.tsx
new file mode 100644
index 0000000000..c7e7e2e18d
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/src/App.tsx
@@ -0,0 +1,14 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import "./style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/14-editor-scrollable/src/style.css b/examples/01-basic/14-editor-scrollable/src/style.css
new file mode 100644
index 0000000000..478d974662
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/src/style.css
@@ -0,0 +1,4 @@
+.bn-editor {
+ height: 500px;
+ overflow: auto;
+}
diff --git a/examples/01-basic/14-editor-scrollable/tsconfig.json b/examples/01-basic/14-editor-scrollable/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/14-editor-scrollable/vite.config.ts b/examples/01-basic/14-editor-scrollable/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/14-editor-scrollable/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/15-shadowdom/.bnexample.json b/examples/01-basic/15-shadowdom/.bnexample.json
new file mode 100644
index 0000000000..e9c8bcb27b
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/15-shadowdom/README.md b/examples/01-basic/15-shadowdom/README.md
new file mode 100644
index 0000000000..caf50933e0
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/README.md
@@ -0,0 +1,7 @@
+# Shadow DOM
+
+This example shows how to render the BlockNote editor inside a Shadow DOM.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/15-shadowdom/index.html b/examples/01-basic/15-shadowdom/index.html
new file mode 100644
index 0000000000..9e5d5d45ad
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Shadow DOM
+
+
+
+
+
+
+
diff --git a/examples/01-basic/15-shadowdom/main.tsx b/examples/01-basic/15-shadowdom/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/15-shadowdom/package.json b/examples/01-basic/15-shadowdom/package.json
new file mode 100644
index 0000000000..bb80c17b37
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-shadowdom",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/15-shadowdom/src/App.tsx b/examples/01-basic/15-shadowdom/src/App.tsx
new file mode 100644
index 0000000000..b16b7e9264
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/src/App.tsx
@@ -0,0 +1,46 @@
+import { BlockNoteView } from "@blocknote/mantine";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import { useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+
+import interCss from "@blocknote/core/fonts/inter.css?inline";
+import mantineCss from "@blocknote/mantine/style.css?inline";
+
+function ShadowWrapper(props: { children: React.ReactNode }) {
+ const host = useRef(null);
+ const [shadowRoot, setShadowRoot] = useState(null);
+
+ useEffect(() => {
+ if (host.current && !shadowRoot) {
+ const root = host.current.shadowRoot || host.current.attachShadow({ mode: "open" });
+ setShadowRoot(root);
+ }
+ }, [shadowRoot]);
+
+ return (
+
+ {shadowRoot &&
+ createPortal(
+ <>
+
+
+ {props.children}
+ >,
+ shadowRoot as any
+ )}
+
+ );
+}
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote();
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+
+ );
+}
diff --git a/examples/01-basic/15-shadowdom/src/vite-env.d.ts b/examples/01-basic/15-shadowdom/src/vite-env.d.ts
new file mode 100644
index 0000000000..4754603a78
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/src/vite-env.d.ts
@@ -0,0 +1,6 @@
+///
+
+declare module "*?inline" {
+ const content: string;
+ export default content;
+}
diff --git a/examples/01-basic/15-shadowdom/tsconfig.json b/examples/01-basic/15-shadowdom/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/15-shadowdom/vite.config.ts b/examples/01-basic/15-shadowdom/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/15-shadowdom/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/16-read-only-editor/.bnexample.json b/examples/01-basic/16-read-only-editor/.bnexample.json
new file mode 100644
index 0000000000..6d4a02dd52
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/16-read-only-editor/README.md b/examples/01-basic/16-read-only-editor/README.md
new file mode 100644
index 0000000000..38d6b91fef
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/README.md
@@ -0,0 +1,9 @@
+# Read-only Editor
+
+This example makes the editor read-only while showing the same content as the [Default Schema Showcase](/examples/basic/default-blocks) example.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Document Structure](/docs/foundations/document-structure)
+- [Default Schema](/docs/foundations/schemas)
diff --git a/examples/01-basic/16-read-only-editor/index.html b/examples/01-basic/16-read-only-editor/index.html
new file mode 100644
index 0000000000..66836dd166
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Read-only Editor
+
+
+
+
+
+
+
diff --git a/examples/01-basic/16-read-only-editor/main.tsx b/examples/01-basic/16-read-only-editor/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/16-read-only-editor/package.json b/examples/01-basic/16-read-only-editor/package.json
new file mode 100644
index 0000000000..c746366d56
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-read-only-editor",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/16-read-only-editor/src/App.tsx b/examples/01-basic/16-read-only-editor/src/App.tsx
new file mode 100644
index 0000000000..b7a1ef9d00
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/src/App.tsx
@@ -0,0 +1,153 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Blocks:",
+ styles: { bold: true },
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Paragraph",
+ },
+ {
+ type: "heading",
+ content: "Heading",
+ },
+ {
+ id: "toggle-heading",
+ type: "heading",
+ props: { isToggleable: true },
+ content: "Toggle Heading",
+ },
+ {
+ type: "quote",
+ content: "Quote",
+ },
+ {
+ type: "bulletListItem",
+ content: "Bullet List Item",
+ },
+ {
+ type: "numberedListItem",
+ content: "Numbered List Item",
+ },
+ {
+ type: "checkListItem",
+ content: "Check List Item",
+ },
+ {
+ id: "toggle-list-item",
+ type: "toggleListItem",
+ content: "Toggle List Item",
+ },
+ {
+ type: "codeBlock",
+ props: { language: "javascript" },
+ content: "console.log('Hello, world!');",
+ },
+ {
+ type: "table",
+ content: {
+ type: "tableContent",
+ rows: [
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ {
+ cells: ["Table Cell", "Table Cell", "Table Cell"],
+ },
+ ],
+ },
+ },
+ {
+ type: "file",
+ },
+ {
+ type: "image",
+ props: {
+ url: "https://placehold.co/332x322.jpg",
+ caption: "From https://placehold.co/332x322.jpg",
+ },
+ },
+ {
+ type: "video",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+ },
+ },
+ {
+ type: "audio",
+ props: {
+ url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ caption:
+ "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+ },
+ },
+ {
+ type: "paragraph",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Inline Content:",
+ styles: { bold: true },
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Styled Text",
+ styles: {
+ bold: true,
+ italic: true,
+ textColor: "red",
+ backgroundColor: "blue",
+ },
+ },
+ {
+ type: "text",
+ text: " ",
+ styles: {},
+ },
+ {
+ type: "link",
+ content: "Link",
+ href: "https://www.blocknotejs.org",
+ },
+ ],
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component and makes it read-only.
+ return ;
+}
diff --git a/examples/01-basic/16-read-only-editor/tsconfig.json b/examples/01-basic/16-read-only-editor/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/16-read-only-editor/vite.config.ts b/examples/01-basic/16-read-only-editor/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/16-read-only-editor/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/17-no-trailing-block/.bnexample.json b/examples/01-basic/17-no-trailing-block/.bnexample.json
new file mode 100644
index 0000000000..e9c8bcb27b
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/17-no-trailing-block/README.md b/examples/01-basic/17-no-trailing-block/README.md
new file mode 100644
index 0000000000..9c63e46fdd
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/README.md
@@ -0,0 +1,7 @@
+# No Trailing Block
+
+This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/17-no-trailing-block/index.html b/examples/01-basic/17-no-trailing-block/index.html
new file mode 100644
index 0000000000..a86933f050
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ No Trailing Block
+
+
+
+
+
+
+
diff --git a/examples/01-basic/17-no-trailing-block/main.tsx b/examples/01-basic/17-no-trailing-block/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/17-no-trailing-block/package.json b/examples/01-basic/17-no-trailing-block/package.json
new file mode 100644
index 0000000000..892bec6a77
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-no-trailing-block",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/17-no-trailing-block/src/App.tsx b/examples/01-basic/17-no-trailing-block/src/App.tsx
new file mode 100644
index 0000000000..ac51ec74b3
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/src/App.tsx
@@ -0,0 +1,14 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ trailingBlock: false,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/17-no-trailing-block/tsconfig.json b/examples/01-basic/17-no-trailing-block/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/17-no-trailing-block/vite.config.ts b/examples/01-basic/17-no-trailing-block/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/01-basic/testing/.bnexample.json b/examples/01-basic/testing/.bnexample.json
new file mode 100644
index 0000000000..dc19f5a930
--- /dev/null
+++ b/examples/01-basic/testing/.bnexample.json
@@ -0,0 +1,4 @@
+{
+ "playground": true,
+ "docs": false
+}
diff --git a/examples/01-basic/testing/README.md b/examples/01-basic/testing/README.md
new file mode 100644
index 0000000000..04d01f3847
--- /dev/null
+++ b/examples/01-basic/testing/README.md
@@ -0,0 +1,3 @@
+# Test Editor
+
+This example is meant for use in end-to-end tests.
diff --git a/examples/01-basic/testing/index.html b/examples/01-basic/testing/index.html
new file mode 100644
index 0000000000..b43d92fb67
--- /dev/null
+++ b/examples/01-basic/testing/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Test Editor
+
+
+
+
+
+
+
diff --git a/examples/01-basic/testing/main.tsx b/examples/01-basic/testing/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/testing/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/testing/package.json b/examples/01-basic/testing/package.json
new file mode 100644
index 0000000000..11ac43e993
--- /dev/null
+++ b/examples/01-basic/testing/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-basic-testing",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/testing/src/App.tsx b/examples/01-basic/testing/src/App.tsx
new file mode 100644
index 0000000000..353e445e91
--- /dev/null
+++ b/examples/01-basic/testing/src/App.tsx
@@ -0,0 +1,15 @@
+import { uploadToTmpFilesDotOrg_DEV_ONLY } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ uploadFile: uploadToTmpFilesDotOrg_DEV_ONLY,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/testing/tsconfig.json b/examples/01-basic/testing/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/testing/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/testing/vite.config.ts b/examples/01-basic/testing/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/testing/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/02-backend/01-file-uploading/.bnexample.json b/examples/02-backend/01-file-uploading/.bnexample.json
new file mode 100644
index 0000000000..4cf4ab84ef
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Intermediate", "Saving/Loading"]
+}
diff --git a/examples/02-backend/01-file-uploading/README.md b/examples/02-backend/01-file-uploading/README.md
new file mode 100644
index 0000000000..7c471cf83a
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/README.md
@@ -0,0 +1,10 @@
+# Upload Files
+
+This example allows users to upload files and use them in the editor. The files are uploaded to [/TMP/Files](https://tmpfiles.org/), and can be used for File, Image, Video, and Audio blocks.
+
+**Try it out:** Click the "Add Image" button and see there's now an "Upload" tab in the toolbar!
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [File Block](/docs/features/blocks/embeds#file)
diff --git a/examples/02-backend/01-file-uploading/index.html b/examples/02-backend/01-file-uploading/index.html
new file mode 100644
index 0000000000..7d5ab8b393
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Upload Files
+
+
+
+
+
+
+
diff --git a/examples/02-backend/01-file-uploading/main.tsx b/examples/02-backend/01-file-uploading/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/02-backend/01-file-uploading/package.json b/examples/02-backend/01-file-uploading/package.json
new file mode 100644
index 0000000000..b12ea90ff6
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-backend-file-uploading",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/02-backend/01-file-uploading/src/App.tsx b/examples/02-backend/01-file-uploading/src/App.tsx
new file mode 100644
index 0000000000..982d0f37a6
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/src/App.tsx
@@ -0,0 +1,42 @@
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+// Uploads a file to tmpfiles.org and returns the URL to the uploaded file.
+async function uploadFile(file: File) {
+ const body = new FormData();
+ body.append("file", file);
+
+ const ret = await fetch("https://tmpfiles.org/api/v1/upload", {
+ method: "POST",
+ body: body,
+ });
+ return (await ret.json()).data.url.replace(
+ "tmpfiles.org/",
+ "tmpfiles.org/dl/",
+ );
+}
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Upload an image using the button below",
+ },
+ {
+ type: "image",
+ },
+ ],
+ uploadFile,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/02-backend/01-file-uploading/tsconfig.json b/examples/02-backend/01-file-uploading/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/02-backend/01-file-uploading/vite.config.ts b/examples/02-backend/01-file-uploading/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/02-backend/01-file-uploading/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/02-backend/02-saving-loading/.bnexample.json b/examples/02-backend/02-saving-loading/.bnexample.json
new file mode 100644
index 0000000000..a6098bc73d
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["Intermediate", "Blocks", "Saving/Loading"]
+}
diff --git a/examples/02-backend/02-saving-loading/README.md b/examples/02-backend/02-saving-loading/README.md
new file mode 100644
index 0000000000..29a853a6bf
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/README.md
@@ -0,0 +1,12 @@
+# Saving & Loading
+
+This example shows how to save the editor contents to local storage whenever a change is made, and load the saved contents when the editor is created.
+
+You can replace the `saveToStorage` and `loadFromStorage` functions with calls to your backend or database.
+
+**Try it out:** Try typing in the editor and reloading the page!
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Getting the Document](/docs/foundations/manipulating-content#reading-blocks)
diff --git a/examples/02-backend/02-saving-loading/index.html b/examples/02-backend/02-saving-loading/index.html
new file mode 100644
index 0000000000..b6c60cab73
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Saving & Loading
+
+
+
+
+
+
+
diff --git a/examples/02-backend/02-saving-loading/main.tsx b/examples/02-backend/02-saving-loading/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/02-backend/02-saving-loading/package.json b/examples/02-backend/02-saving-loading/package.json
new file mode 100644
index 0000000000..b38ebfcd69
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-backend-saving-loading",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/02-backend/02-saving-loading/src/App.tsx b/examples/02-backend/02-saving-loading/src/App.tsx
new file mode 100644
index 0000000000..5fe11c206b
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/src/App.tsx
@@ -0,0 +1,56 @@
+import { Block, BlockNoteEditor, PartialBlock } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useEffect, useMemo, useState } from "react";
+
+async function saveToStorage(jsonBlocks: Block[]) {
+ // Save contents to local storage. You might want to debounce this or replace
+ // with a call to your API / database.
+ localStorage.setItem("editorContent", JSON.stringify(jsonBlocks));
+}
+
+async function loadFromStorage() {
+ // Gets the previously stored editor contents.
+ const storageString = localStorage.getItem("editorContent");
+ return storageString
+ ? (JSON.parse(storageString) as PartialBlock[])
+ : undefined;
+}
+
+export default function App() {
+ const [initialContent, setInitialContent] = useState<
+ PartialBlock[] | undefined | "loading"
+ >("loading");
+
+ // Loads the previously stored editor contents.
+ useEffect(() => {
+ loadFromStorage().then((content) => {
+ setInitialContent(content);
+ });
+ }, []);
+
+ // Creates a new editor instance.
+ // We use useMemo + createBlockNoteEditor instead of useCreateBlockNote so we
+ // can delay the creation of the editor until the initial content is loaded.
+ const editor = useMemo(() => {
+ if (initialContent === "loading") {
+ return undefined;
+ }
+ return BlockNoteEditor.create({ initialContent });
+ }, [initialContent]);
+
+ if (editor === undefined) {
+ return "Loading content...";
+ }
+
+ // Renders the editor instance.
+ return (
+ {
+ saveToStorage(editor.document);
+ }}
+ />
+ );
+}
diff --git a/examples/02-backend/02-saving-loading/tsconfig.json b/examples/02-backend/02-saving-loading/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/02-backend/02-saving-loading/vite.config.ts b/examples/02-backend/02-saving-loading/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/02-backend/02-saving-loading/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/02-backend/03-s3/.bnexample.json b/examples/02-backend/03-s3/.bnexample.json
new file mode 100644
index 0000000000..7fa3b8f62c
--- /dev/null
+++ b/examples/02-backend/03-s3/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Intermediate", "Saving/Loading"],
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.609.0",
+ "@aws-sdk/s3-request-presigner": "^3.609.0"
+ },
+ "pro": true
+}
diff --git a/examples/02-backend/03-s3/README.md b/examples/02-backend/03-s3/README.md
new file mode 100644
index 0000000000..f9970e93cf
--- /dev/null
+++ b/examples/02-backend/03-s3/README.md
@@ -0,0 +1,10 @@
+# Upload Files to AWS S3
+
+This example allows users to upload files to an AWS S3 bucket and use them in the editor. The files can be used for File, Image, Video, and Audio blocks.
+
+**Try it out:** Click the "Add Image" button and see there's now an "Upload" tab in the toolbar!
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [File Block](/docs/features/blocks/embeds#file)
diff --git a/examples/02-backend/03-s3/index.html b/examples/02-backend/03-s3/index.html
new file mode 100644
index 0000000000..853f6527c4
--- /dev/null
+++ b/examples/02-backend/03-s3/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Upload Files to AWS S3
+
+
+
+
+
+
+
diff --git a/examples/02-backend/03-s3/main.tsx b/examples/02-backend/03-s3/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/02-backend/03-s3/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/02-backend/03-s3/package.json b/examples/02-backend/03-s3/package.json
new file mode 100644
index 0000000000..0ff100fa90
--- /dev/null
+++ b/examples/02-backend/03-s3/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-backend-s3",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@aws-sdk/client-s3": "^3.609.0",
+ "@aws-sdk/s3-request-presigner": "^3.609.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/02-backend/03-s3/src/App.tsx b/examples/02-backend/03-s3/src/App.tsx
new file mode 100644
index 0000000000..6f6731ec7e
--- /dev/null
+++ b/examples/02-backend/03-s3/src/App.tsx
@@ -0,0 +1,150 @@
+import {
+ GetObjectCommand,
+ // GetObjectCommand,
+ PutObjectCommand,
+ S3Client,
+} from "@aws-sdk/client-s3";
+import {
+ S3RequestPresigner,
+ getSignedUrl,
+} from "@aws-sdk/s3-request-presigner";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+/**
+ * SERVER Code. Normally, this part would be implemented on your server, so you
+ * can hide your AWS credentials and control access to your S3 bucket.
+ *
+ * In our demo, we are using a public S3 bucket so everything can be done in
+ * the client.
+ */
+
+// Set up the AWS SDK.
+const client = new S3Client({
+ region: "us-east-1",
+ credentials: {
+ accessKeyId: "",
+ secretAccessKey: "",
+ },
+});
+
+/**
+ * The method on the server that generates a pre-signed URL for a PUT request.
+ */
+const SERVER_createPresignedUrlPUT = (opts: {
+ bucket: string;
+ key: string;
+}) => {
+ // This function would normally be implemented on your server. Of course, you
+ // should make sure the calling user is authenticated, etc.
+ const { bucket, key } = opts;
+ const command = new PutObjectCommand({
+ Bucket: bucket,
+ Key: key,
+ });
+ return getSignedUrl(client, command, { expiresIn: 3600 });
+};
+
+/**
+ * The method on the server that generates a pre-signed URL for a GET request.
+ */
+const SERVER_createPresignedUrlGET = (opts: {
+ bucket: string;
+ key: string;
+}) => {
+ // This function would normally be implemented on your server. Of course, you
+ // should make sure the calling user is authenticated, etc.
+ const { bucket, key } = opts;
+ const command = new GetObjectCommand({
+ Bucket: bucket,
+ Key: key,
+ });
+ return getSignedUrl(client, command, { expiresIn: 3600 });
+};
+
+/**
+ * CLIENT code
+ */
+export default function App() {
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Upload an image to S3 using the button below",
+ },
+ {
+ type: "image",
+ },
+ ],
+ uploadFile: async (file) => {
+ /**
+ * This function is called by BlockNote whenever it wants to upload a
+ * file. In this implementation, we are uploading the file to an S3 bucket
+ * by first requesting an upload URL from the server.
+ */
+ const bucket = "blocknote-demo";
+ const key = file.name;
+
+ // Get a URL to upload to from the server.
+ const signedUrl = await SERVER_createPresignedUrlPUT({
+ bucket,
+ key,
+ });
+
+ const headers: any = {};
+ if (file?.type) {
+ // S3 requires setting the correct content type.
+ headers["Content-Type"] = file!.type || "application/octet-stream";
+ }
+
+ // Actually upload the file.
+ const uploaded = await fetch(signedUrl, {
+ method: "PUT",
+ body: file,
+ headers,
+ });
+
+ if (!uploaded.ok) {
+ throw new Error("Failed to upload file");
+ }
+
+ // We store the URL in a custom format, in this case s3://bucket/key.
+ // We'll subsequently parse this URL in the resolveFileUrl function.
+ return `s3://${bucket}/${key}`;
+ },
+ resolveFileUrl: async (url) => {
+ /**
+ * This function is called by BlockNote whenever it needs to use URL from
+ * a file block. For example, when displaying an image or downloading a
+ * file.
+ *
+ * In this implementation, we are parsing our custom format and return a
+ * signed URL from our backend.
+ */
+ if (url.startsWith("s3:")) {
+ // it's our custom format, request a signed url from the backend
+ const [, , bucket, key] = url.split("/", 4);
+ const presignedUrl = await SERVER_createPresignedUrlGET({
+ bucket,
+ key,
+ });
+ return presignedUrl;
+ }
+
+ return url;
+ },
+ });
+
+ // Renders the editor instance.
+ return ;
+}
+
+// This is a hack to make sure the S3RequestPresigner is not used (our demo
+// bucket is configured for anonymous access). Remove this in your own code.
+S3RequestPresigner.prototype.presign = (request: any) => request;
diff --git a/examples/02-backend/03-s3/tsconfig.json b/examples/02-backend/03-s3/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/02-backend/03-s3/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/02-backend/03-s3/vite.config.ts b/examples/02-backend/03-s3/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/02-backend/03-s3/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/02-backend/04-rendering-static-documents/.bnexample.json b/examples/02-backend/04-rendering-static-documents/.bnexample.json
new file mode 100644
index 0000000000..a4986d761b
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/.bnexample.json
@@ -0,0 +1,9 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["server"],
+ "dependencies": {
+ "@blocknote/server-util": "latest"
+ }
+}
diff --git a/examples/02-backend/04-rendering-static-documents/README.md b/examples/02-backend/04-rendering-static-documents/README.md
new file mode 100644
index 0000000000..c24c4ca6be
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/README.md
@@ -0,0 +1,7 @@
+# Rendering static documents
+
+This example shows how you can use HTML exported using the `blocksToFullHTML` and render it as a static document (a view-only document, without the editor). You can use this for example if you use BlockNote to edit blog posts in a CMS, but want to display non-editable static, published pages to end-users.
+
+**Relevant Docs:**
+
+- [Server-side processing](/docs/features/server-processing)
diff --git a/examples/02-backend/04-rendering-static-documents/index.html b/examples/02-backend/04-rendering-static-documents/index.html
new file mode 100644
index 0000000000..a62a394dd5
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Rendering static documents
+
+
+
+
+
+
+
diff --git a/examples/02-backend/04-rendering-static-documents/main.tsx b/examples/02-backend/04-rendering-static-documents/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/02-backend/04-rendering-static-documents/package.json b/examples/02-backend/04-rendering-static-documents/package.json
new file mode 100644
index 0000000000..619434055e
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-backend-rendering-static-documents",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/server-util": "latest"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/02-backend/04-rendering-static-documents/src/App.tsx b/examples/02-backend/04-rendering-static-documents/src/App.tsx
new file mode 100644
index 0000000000..3a5db9891b
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/src/App.tsx
@@ -0,0 +1,67 @@
+import "@blocknote/core/fonts/inter.css";
+import "@blocknote/mantine/style.css";
+
+/**
+ On Server Side, you can use the ServerBlockNoteEditor to render BlockNote documents to HTML. e.g.:
+
+ import { ServerBlockNoteEditor } from "@blocknote/server-util";
+
+ const editor = ServerBlockNoteEditor.create();
+ const html = await editor.blocksToFullHTML(document);
+
+You can then use render this HTML as a static page or send it to the client. Make sure to include the editor stylesheets:
+
+ import "@blocknote/core/fonts/inter.css";
+ // Depending on the UI library you're using, you may want to use `react`,
+ // `mantine`, etc instead of `core`.
+ import "@blocknote/core/style.css";
+
+This example has the HTML hard-coded, but shows at least how the document will be rendered when the appropriate style sheets are loaded.
+ */
+
+export default function App() {
+ // This HTML is generated by the ServerBlockNoteEditor.blocksToFullHTML method
+ const html = `
+
+
+
+
+ Heading 2
+
+
+
+
+
+
+
Paragraph
+
+
+
+
+
+
+
list item
+
+
+
+
+
+
+
+`;
+
+ // Renders the editor instance using a React component.
+ return (
+ // To make the HTML look identical to the editor, we need to add these two
+ // wrapping divs to the exported blocks. You need will need to add
+ // additional class names/attributes depend on the UI library you're using,
+ // whether you want to show light or dark more, etc. It's easiest to just
+ // check the rendered editor HTML to see what you need to add.
+
+
+
+ );
+}
diff --git a/examples/02-backend/04-rendering-static-documents/tsconfig.json b/examples/02-backend/04-rendering-static-documents/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/02-backend/04-rendering-static-documents/vite.config.ts b/examples/02-backend/04-rendering-static-documents/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/02-backend/04-rendering-static-documents/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/01-ui-elements-remove/.bnexample.json b/examples/03-ui-components/01-ui-elements-remove/.bnexample.json
new file mode 100644
index 0000000000..fb472bdb77
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": false,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Basic", "UI Components"]
+}
diff --git a/examples/03-ui-components/01-ui-elements-remove/README.md b/examples/03-ui-components/01-ui-elements-remove/README.md
new file mode 100644
index 0000000000..625fd0321b
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/README.md
@@ -0,0 +1,7 @@
+# Removing UI Elements
+
+In this example, we remove all menus & toolbars, leaving only the editor.
+
+**Relevant Docs:**
+
+- [UI Components](/docs/react/components)
diff --git a/examples/03-ui-components/01-ui-elements-remove/index.html b/examples/03-ui-components/01-ui-elements-remove/index.html
new file mode 100644
index 0000000000..7b475d63b8
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Removing UI Elements
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/01-ui-elements-remove/main.tsx b/examples/03-ui-components/01-ui-elements-remove/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/01-ui-elements-remove/package.json b/examples/03-ui-components/01-ui-elements-remove/package.json
new file mode 100644
index 0000000000..429291840f
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-ui-components-ui-elements-remove",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/01-ui-elements-remove/src/App.tsx b/examples/03-ui-components/01-ui-elements-remove/src/App.tsx
new file mode 100644
index 0000000000..6f8b39a75f
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/src/App.tsx
@@ -0,0 +1,40 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content:
+ "There are no menus or toolbars in this editor, but you can still markup text using keyboard shortcuts.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Try making text bold with Ctrl+B/Cmd+B or undo with Ctrl+Z/Cmd+Z.",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+ );
+}
diff --git a/examples/03-ui-components/01-ui-elements-remove/tsconfig.json b/examples/03-ui-components/01-ui-elements-remove/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/01-ui-elements-remove/vite.config.ts b/examples/03-ui-components/01-ui-elements-remove/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/01-ui-elements-remove/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/.bnexample.json b/examples/03-ui-components/02-formatting-toolbar-buttons/.bnexample.json
new file mode 100644
index 0000000000..e38b6fb26a
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": [
+ "Intermediate",
+ "Inline Content",
+ "UI Components",
+ "Formatting Toolbar"
+ ]
+}
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/README.md b/examples/03-ui-components/02-formatting-toolbar-buttons/README.md
new file mode 100644
index 0000000000..a315178355
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/README.md
@@ -0,0 +1,11 @@
+# Adding Formatting Toolbar Buttons
+
+In this example, we add a blue text/background color and code style button to the Formatting Toolbar. We also make sure it only shows up when some text is selected.
+
+**Try it out:** Select some text to open the Formatting Toolbar, and click one of the new buttons!
+
+**Relevant Docs:**
+
+- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)
+- [Manipulating Inline Content](/docs/reference/editor/manipulating-content)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/index.html b/examples/03-ui-components/02-formatting-toolbar-buttons/index.html
new file mode 100644
index 0000000000..c347065176
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding Formatting Toolbar Buttons
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/main.tsx b/examples/03-ui-components/02-formatting-toolbar-buttons/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/package.json b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json
new file mode 100644
index 0000000000..430ff81344
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-ui-components-formatting-toolbar-buttons",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx b/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx
new file mode 100644
index 0000000000..f6bb83c2e1
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx
@@ -0,0 +1,116 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ BasicTextStyleButton,
+ BlockTypeSelect,
+ ColorStyleButton,
+ CreateLinkButton,
+ FileCaptionButton,
+ FileReplaceButton,
+ FormattingToolbar,
+ FormattingToolbarController,
+ NestBlockButton,
+ TextAlignButton,
+ UnnestBlockButton,
+ useCreateBlockNote,
+} from "@blocknote/react";
+
+import { BlueButton } from "./BlueButton";
+
+const CustomFormattingToolbar = () => (
+
+
+
+ {/* Extra button to toggle blue text & background */}
+
+
+
+
+
+
+
+
+
+ {/* Extra button to toggle code styles */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "You can now toggle ",
+ styles: {},
+ },
+ {
+ type: "text",
+ text: "blue",
+ styles: { textColor: "blue", backgroundColor: "blue" },
+ },
+ {
+ type: "text",
+ text: " and ",
+ styles: {},
+ },
+ {
+ type: "text",
+ text: "code",
+ styles: { code: true },
+ },
+ {
+ type: "text",
+ text: " styles with new buttons in the Formatting Toolbar",
+ styles: {},
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Select some text to try them out",
+ },
+ {
+ type: "image",
+ props: {
+ url: "https://placehold.co/332x322.jpg",
+ },
+ },
+ {
+ type: "paragraph",
+ content:
+ "Notice that the buttons don't appear when the image block above is selected, as it has no inline content.",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/src/BlueButton.tsx b/examples/03-ui-components/02-formatting-toolbar-buttons/src/BlueButton.tsx
new file mode 100644
index 0000000000..eedb7c6685
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/src/BlueButton.tsx
@@ -0,0 +1,45 @@
+import "@blocknote/mantine/style.css";
+import {
+ useBlockNoteEditor,
+ useComponentsContext,
+ useEditorState,
+ useSelectedBlocks,
+} from "@blocknote/react";
+
+// Custom Formatting Toolbar Button to toggle blue text & background color.
+export function BlueButton() {
+ const editor = useBlockNoteEditor();
+
+ const Components = useComponentsContext()!;
+
+ // Tracks whether the text & background are both blue.
+ const isSelected = useEditorState({
+ editor,
+ selector: ({ editor }) =>
+ editor.getActiveStyles().textColor === "blue" &&
+ editor.getActiveStyles().backgroundColor === "blue",
+ });
+
+ // Doesn't render unless a at least one block with inline content is
+ // selected. You can use a similar pattern of returning `null` to
+ // conditionally render buttons based on the editor state.
+ const blocks = useSelectedBlocks();
+ if (blocks.filter((block) => block.content !== undefined).length === 0) {
+ return null;
+ }
+
+ return (
+ {
+ editor.toggleStyles({
+ textColor: "blue",
+ backgroundColor: "blue",
+ });
+ }}
+ isSelected={isSelected}
+ >
+ Blue
+
+ );
+}
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/tsconfig.json b/examples/03-ui-components/02-formatting-toolbar-buttons/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/02-formatting-toolbar-buttons/vite.config.ts b/examples/03-ui-components/02-formatting-toolbar-buttons/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json
new file mode 100644
index 0000000000..9d554dc51c
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/.bnexample.json
@@ -0,0 +1,16 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": [
+ "Intermediate",
+ "Blocks",
+ "UI Components",
+ "Formatting Toolbar",
+ "Custom Schemas"
+ ],
+ "dependencies": {
+ "@mantine/core": "^9.0.2",
+ "react-icons": "^5.5.0"
+ }
+}
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/README.md b/examples/03-ui-components/03-formatting-toolbar-block-type-items/README.md
new file mode 100644
index 0000000000..6a4a20aae9
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/README.md
@@ -0,0 +1,11 @@
+# Adding Block Type Select Items
+
+In this example, we add an item to the Block Type Select, so that it works for a custom alert block we create.
+
+**Try it out:** Select some text to open the Formatting Toolbar, and click "Alert" in the Block Type Select to change the selected block!
+
+**Relevant Docs:**
+
+- [Changing Block Type Select Items](/docs/react/components/formatting-toolbar)
+- [Custom Block Types](/docs/features/custom-schemas/custom-blocks)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/index.html b/examples/03-ui-components/03-formatting-toolbar-block-type-items/index.html
new file mode 100644
index 0000000000..661579e8df
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding Block Type Select Items
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/main.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json
new file mode 100644
index 0000000000..4b550aaab2
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-ui-components-formatting-toolbar-block-type-items",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-icons": "^5.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx
new file mode 100644
index 0000000000..4b2af03fbe
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/Alert.tsx
@@ -0,0 +1,120 @@
+import { defaultProps } from "@blocknote/core";
+import { createReactBlockSpec } from "@blocknote/react";
+import { Menu } from "@mantine/core";
+import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";
+
+import "./styles.css";
+
+// The types of alerts that users can choose from.
+export const alertTypes = [
+ {
+ title: "Warning",
+ value: "warning",
+ icon: MdError,
+ color: "#e69819",
+ backgroundColor: {
+ light: "#fff6e6",
+ dark: "#805d20",
+ },
+ },
+ {
+ title: "Error",
+ value: "error",
+ icon: MdCancel,
+ color: "#d80d0d",
+ backgroundColor: {
+ light: "#ffe6e6",
+ dark: "#802020",
+ },
+ },
+ {
+ title: "Info",
+ value: "info",
+ icon: MdInfo,
+ color: "#507aff",
+ backgroundColor: {
+ light: "#e6ebff",
+ dark: "#203380",
+ },
+ },
+ {
+ title: "Success",
+ value: "success",
+ icon: MdCheckCircle,
+ color: "#0bc10b",
+ backgroundColor: {
+ light: "#e6ffe6",
+ dark: "#208020",
+ },
+ },
+] as const;
+
+// The Alert block.
+export const Alert = createReactBlockSpec(
+ {
+ type: "alert",
+ propSchema: {
+ textAlignment: defaultProps.textAlignment,
+ textColor: defaultProps.textColor,
+ type: {
+ default: "warning",
+ values: ["warning", "error", "info", "success"],
+ },
+ },
+ content: "inline",
+ },
+ {
+ render: (props) => {
+ const alertType = alertTypes.find(
+ (a) => a.value === props.block.props.type,
+ )!;
+ const Icon = alertType.icon;
+ return (
+
+ {/*Icon which opens a menu to choose the Alert type*/}
+
+ {/*Rich text field for user to type in*/}
+
+
+ );
+ },
+ },
+);
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx
new file mode 100644
index 0000000000..97b5836bfb
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/App.tsx
@@ -0,0 +1,82 @@
+import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbarController,
+ blockTypeSelectItems,
+ useBlockNoteEditor,
+ useCreateBlockNote,
+ BlockTypeSelectItem,
+ FormattingToolbar,
+} from "@blocknote/react";
+
+import { RiAlertFill } from "react-icons/ri";
+import { Alert } from "./Alert";
+
+// Our schema with block specs, which contain the configs and implementations for
+// blocks that we want our editor to use.
+const schema = BlockNoteSchema.create({
+ blockSpecs: {
+ // Adds all default blocks.
+ ...defaultBlockSpecs,
+ // Adds the Alert block.
+ alert: Alert(),
+ },
+});
+
+const CustomFormattingToolbar = () => {
+ const editor = useBlockNoteEditor<
+ typeof schema.blockSchema,
+ typeof schema.inlineContentSchema,
+ typeof schema.styleSchema
+ >();
+
+ return (
+ // Uses the default Formatting Toolbar.
+
+ );
+};
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ schema,
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Try selecting some text - you'll see the new 'Alert' item in the Block Type Select",
+ },
+ {
+ type: "alert",
+ content:
+ "Or select text in this alert - the Block Type Select also appears",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+ {/* Replaces the default Formatting Toolbar */}
+
+
+ );
+}
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/styles.css b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/styles.css
new file mode 100644
index 0000000000..a529138eee
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/src/styles.css
@@ -0,0 +1,74 @@
+.alert {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-grow: 1;
+ border-radius: 4px;
+ min-height: 48px;
+ padding: 4px;
+}
+
+.alert[data-alert-type="warning"] {
+ background-color: #fff6e6;
+}
+
+.alert[data-alert-type="error"] {
+ background-color: #ffe6e6;
+}
+
+.alert[data-alert-type="info"] {
+ background-color: #e6ebff;
+}
+
+.alert[data-alert-type="success"] {
+ background-color: #e6ffe6;
+}
+
+[data-color-scheme="dark"] .alert[data-alert-type="warning"] {
+ background-color: #805d20;
+}
+
+[data-color-scheme="dark"] .alert[data-alert-type="error"] {
+ background-color: #802020;
+}
+
+[data-color-scheme="dark"] .alert[data-alert-type="info"] {
+ background-color: #203380;
+}
+
+[data-color-scheme="dark"] .alert[data-alert-type="success"] {
+ background-color: #208020;
+}
+
+.alert-icon-wrapper {
+ border-radius: 16px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: 12px;
+ margin-right: 12px;
+ height: 18px;
+ width: 18px;
+ user-select: none;
+ cursor: pointer;
+}
+
+.alert-icon[data-alert-icon-type="warning"] {
+ color: #e69819;
+}
+
+.alert-icon[data-alert-icon-type="error"] {
+ color: #d80d0d;
+}
+
+.alert-icon[data-alert-icon-type="info"] {
+ color: #507aff;
+}
+
+.alert-icon[data-alert-icon-type="success"] {
+ color: #0bc10b;
+}
+
+.inline-content {
+ flex-grow: 1;
+}
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/tsconfig.json b/examples/03-ui-components/03-formatting-toolbar-block-type-items/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/03-formatting-toolbar-block-type-items/vite.config.ts b/examples/03-ui-components/03-formatting-toolbar-block-type-items/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/03-formatting-toolbar-block-type-items/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/04-side-menu-buttons/.bnexample.json b/examples/03-ui-components/04-side-menu-buttons/.bnexample.json
new file mode 100644
index 0000000000..2e64fc3cdb
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/.bnexample.json
@@ -0,0 +1,9 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Intermediate", "Blocks", "UI Components", "Block Side Menu"],
+ "dependencies": {
+ "react-icons": "^5.5.0"
+ }
+}
diff --git a/examples/03-ui-components/04-side-menu-buttons/README.md b/examples/03-ui-components/04-side-menu-buttons/README.md
new file mode 100644
index 0000000000..da83c56399
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/README.md
@@ -0,0 +1,11 @@
+# Adding Block Side Menu Buttons
+
+In this example, we replace the button to add a block in the Block Side Menu, with a button to remove the hovered block.
+
+**Try it out:** Hover a block to open the Block Side Menu, and click the new button!
+
+**Relevant Docs:**
+
+- [Changing the Block Side Menu](/docs/react/components/side-menu)
+- [Removing Blocks](/docs/reference/editor/manipulating-content)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/04-side-menu-buttons/index.html b/examples/03-ui-components/04-side-menu-buttons/index.html
new file mode 100644
index 0000000000..bc614baf60
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding Block Side Menu Buttons
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/04-side-menu-buttons/main.tsx b/examples/03-ui-components/04-side-menu-buttons/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/04-side-menu-buttons/package.json b/examples/03-ui-components/04-side-menu-buttons/package.json
new file mode 100644
index 0000000000..c3de6a8993
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-ui-components-side-menu-buttons",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-icons": "^5.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/04-side-menu-buttons/src/App.tsx b/examples/03-ui-components/04-side-menu-buttons/src/App.tsx
new file mode 100644
index 0000000000..29a79fbbc9
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/src/App.tsx
@@ -0,0 +1,47 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ DragHandleButton,
+ SideMenu,
+ SideMenuController,
+ SideMenuProps,
+ useCreateBlockNote,
+} from "@blocknote/react";
+
+import { RemoveBlockButton } from "./RemoveBlockButton";
+
+const CustomSideMenu = (props: SideMenuProps) => (
+
+ {/* Button which removes the hovered block. */}
+
+
+
+);
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "<- Notice the new button in the side menu",
+ },
+ {
+ type: "paragraph",
+ content: "Click it to remove the hovered block",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/04-side-menu-buttons/src/RemoveBlockButton.tsx b/examples/03-ui-components/04-side-menu-buttons/src/RemoveBlockButton.tsx
new file mode 100644
index 0000000000..b5e2d058a4
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/src/RemoveBlockButton.tsx
@@ -0,0 +1,37 @@
+import {} from "@blocknote/core";
+import { SideMenuExtension } from "@blocknote/core/extensions";
+import {
+ useBlockNoteEditor,
+ useComponentsContext,
+ useExtensionState,
+} from "@blocknote/react";
+import { MdDelete } from "react-icons/md";
+
+// Custom Side Menu button to remove the hovered block.
+export function RemoveBlockButton() {
+ const editor = useBlockNoteEditor();
+
+ const Components = useComponentsContext()!;
+
+ const block = useExtensionState(SideMenuExtension, {
+ selector: (state) => state?.block,
+ });
+
+ if (!block) {
+ return null;
+ }
+
+ return (
+ {
+ editor.removeBlocks([block]);
+ }}
+ />
+ }
+ />
+ );
+}
diff --git a/examples/03-ui-components/04-side-menu-buttons/tsconfig.json b/examples/03-ui-components/04-side-menu-buttons/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/04-side-menu-buttons/vite.config.ts b/examples/03-ui-components/04-side-menu-buttons/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/04-side-menu-buttons/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/.bnexample.json b/examples/03-ui-components/05-side-menu-drag-handle-items/.bnexample.json
new file mode 100644
index 0000000000..2e64fc3cdb
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/.bnexample.json
@@ -0,0 +1,9 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Intermediate", "Blocks", "UI Components", "Block Side Menu"],
+ "dependencies": {
+ "react-icons": "^5.5.0"
+ }
+}
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/README.md b/examples/03-ui-components/05-side-menu-drag-handle-items/README.md
new file mode 100644
index 0000000000..467c91ebd6
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/README.md
@@ -0,0 +1,11 @@
+# Adding Drag Handle Menu Items
+
+In this example, we add an item to the Drag Handle Menu, which resets the hovered block to a paragraph.
+
+**Try it out:** Hover a block to open the Block Side Menu, and click "Reset Type" in the Drag Handle Menu to reset the selected block!
+
+**Relevant Docs:**
+
+- [Changing Drag Handle Menu Items](/docs/react/components/side-menu)
+- [Updating Blocks](/docs/reference/editor/manipulating-content)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/index.html b/examples/03-ui-components/05-side-menu-drag-handle-items/index.html
new file mode 100644
index 0000000000..b6923ca7c1
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding Drag Handle Menu Items
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/main.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/package.json b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json
new file mode 100644
index 0000000000..15cea76cc7
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-ui-components-side-menu-drag-handle-items",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-icons": "^5.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx
new file mode 100644
index 0000000000..ec50019e0c
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx
@@ -0,0 +1,58 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ BlockColorsItem,
+ DragHandleMenu,
+ RemoveBlockItem,
+ SideMenu,
+ SideMenuController,
+ SideMenuProps,
+ useCreateBlockNote,
+} from "@blocknote/react";
+
+import { ResetBlockTypeItem } from "./ResetBlockTypeItem";
+
+// To avoid rendering issues, it's good practice to define your custom drag
+// handle menu in a separate component, instead of inline within the `sideMenu`
+// prop of `SideMenuController`.
+const CustomDragHandleMenu = () => (
+
+ Delete
+ Colors
+ {/* Item which resets the hovered block's type. */}
+ Reset Type
+
+);
+
+const CustomSideMenu = (props: SideMenuProps) => (
+
+);
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "<- Click the Drag Handle to see the new item",
+ },
+ {
+ type: "bulletListItem",
+ content:
+ "Try resetting this block's type using the new Drag Handle Menu item",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/src/ResetBlockTypeItem.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/src/ResetBlockTypeItem.tsx
new file mode 100644
index 0000000000..20fad5d1a1
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/src/ResetBlockTypeItem.tsx
@@ -0,0 +1,32 @@
+import {} from "@blocknote/core";
+import { SideMenuExtension } from "@blocknote/core/extensions";
+import {
+ useBlockNoteEditor,
+ useComponentsContext,
+ useExtensionState,
+} from "@blocknote/react";
+import { ReactNode } from "react";
+
+export function ResetBlockTypeItem(props: { children: ReactNode }) {
+ const editor = useBlockNoteEditor();
+
+ const Components = useComponentsContext()!;
+
+ const block = useExtensionState(SideMenuExtension, {
+ selector: (state) => state?.block,
+ });
+
+ if (!block) {
+ return null;
+ }
+
+ return (
+ {
+ editor.updateBlock(block, { type: "paragraph" });
+ }}
+ >
+ {props.children}
+
+ );
+}
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/tsconfig.json b/examples/03-ui-components/05-side-menu-drag-handle-items/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/vite.config.ts b/examples/03-ui-components/05-side-menu-drag-handle-items/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/.bnexample.json b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/.bnexample.json
new file mode 100644
index 0000000000..26081354ce
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/.bnexample.json
@@ -0,0 +1,15 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": [
+ "Intermediate",
+ "Blocks",
+ "UI Components",
+ "Suggestion Menus",
+ "Slash Menu"
+ ],
+ "dependencies": {
+ "react-icons": "^5.5.0"
+ }
+}
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/README.md b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/README.md
new file mode 100644
index 0000000000..13336c828e
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/README.md
@@ -0,0 +1,12 @@
+# Adding Slash Menu Items
+
+In this example, we add an item to the Slash Menu, which adds a new block below with a bold "Hello World" string.
+
+**Try it out:** Press the "/" key to open the Slash Menu and select the new item!
+
+**Relevant Docs:**
+
+- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)
+- [Getting Text Cursor Position](/docs/reference/editor/cursor-selections)
+- [Inserting New Blocks](/docs/reference/editor/manipulating-content)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/index.html b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/index.html
new file mode 100644
index 0000000000..2f89240213
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding Slash Menu Items
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/main.tsx b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json
new file mode 100644
index 0000000000..0bddd94672
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-ui-components-suggestion-menus-slash-menu-items",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-icons": "^5.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx
new file mode 100644
index 0000000000..906000401a
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/src/App.tsx
@@ -0,0 +1,74 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import {
+ filterSuggestionItems,
+ insertOrUpdateBlockForSlashMenu,
+} from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ DefaultReactSuggestionItem,
+ getDefaultReactSlashMenuItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { HiOutlineGlobeAlt } from "react-icons/hi";
+
+// Custom Slash Menu item to insert a block after the current one.
+const insertHelloWorldItem = (editor: BlockNoteEditor) => ({
+ title: "Insert Hello World",
+ onItemClick: () =>
+ // If the block containing the text caret is empty, `insertOrUpdateBlock`
+ // changes its type to the provided block. Otherwise, it inserts the new
+ // block below and moves the text caret to it. We use this function with
+ // a block containing 'Hello World' in bold.
+ insertOrUpdateBlockForSlashMenu(editor, {
+ type: "paragraph",
+ content: [{ type: "text", text: "Hello World", styles: { bold: true } }],
+ }),
+ aliases: ["helloworld", "hw"],
+ group: "Other",
+ icon: ,
+ subtext: "Used to insert a block with 'Hello World' below.",
+});
+
+// List containing all default Slash Menu Items, as well as our custom one.
+const getCustomSlashMenuItems = (
+ editor: BlockNoteEditor,
+): DefaultReactSuggestionItem[] => [
+ ...getDefaultReactSlashMenuItems(editor),
+ insertHelloWorldItem(editor),
+];
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Press the '/' key to open the Slash Menu",
+ },
+ {
+ type: "paragraph",
+ content: "Notice the new 'Insert Hello World' item - try it out!",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+ filterSuggestionItems(getCustomSlashMenuItems(editor), query)
+ }
+ />
+
+ );
+}
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/tsconfig.json b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/06-suggestion-menus-slash-menu-items/vite.config.ts b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/06-suggestion-menus-slash-menu-items/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/.bnexample.json b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/.bnexample.json
new file mode 100644
index 0000000000..bd1646adb9
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": [
+ "Intermediate",
+ "UI Components",
+ "Suggestion Menus",
+ "Slash Menu",
+ "Appearance & Styling"
+ ]
+}
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/README.md b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/README.md
new file mode 100644
index 0000000000..31831ebf83
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/README.md
@@ -0,0 +1,10 @@
+# Replacing Slash Menu Component
+
+In this example, we replace the default Slash Menu component with a basic custom one.
+
+**Try it out:** Press the "/" key to see the new Slash Menu!
+
+**Relevant Docs:**
+
+- [Replacing the Slash Menu Component](/docs/react/components/suggestion-menus)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/index.html b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/index.html
new file mode 100644
index 0000000000..60c4faad55
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Replacing Slash Menu Component
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/main.tsx b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json
new file mode 100644
index 0000000000..b7226bd31d
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blocknote/example-ui-components-suggestion-menus-slash-menu-component",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx
new file mode 100644
index 0000000000..c7af9b48a1
--- /dev/null
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/src/App.tsx
@@ -0,0 +1,63 @@
+import "@blocknote/core/fonts/inter.css";
+import {
+ DefaultReactSuggestionItem,
+ SuggestionMenuController,
+ SuggestionMenuProps,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import "./styles.css";
+
+// Custom component to replace the default Slash Menu.
+function CustomSlashMenu(
+ props: SuggestionMenuProps,
+) {
+ return (
+
+ {/* To make the static HTML look identical to the editor, we need to
+ add these two wrapping divs to the exported blocks. These mock the
+ wrapping elements of a BlockNote editor, and are needed as the
+ exported HTML only holds the contents of the editor. You need will
+ need to add additional class names/attributes depend on the UI
+ library you're using, whether you want to show light or dark mode,
+ etc. It's easiest to just copy the class names and HTML attributes
+ from an actual BlockNote editor. */}
+
+
+ );
+}
diff --git a/examples/07-collaboration/05-comments/src/SettingsSelect.tsx b/examples/07-collaboration/05-comments/src/SettingsSelect.tsx
new file mode 100644
index 0000000000..0dfc79dc3f
--- /dev/null
+++ b/examples/07-collaboration/05-comments/src/SettingsSelect.tsx
@@ -0,0 +1,24 @@
+import { ComponentProps, useComponentsContext } from "@blocknote/react";
+
+// This component is used to display a selection dropdown with a label. By using
+// the useComponentsContext hook, we can create it out of existing components
+// within the same UI library that `BlockNoteView` uses (Mantine, Ariakit, or
+// ShadCN), to match the design of the editor.
+export const SettingsSelect = (props: {
+ label: string;
+ items: ComponentProps["FormattingToolbar"]["Select"]["items"];
+}) => {
+ const Components = useComponentsContext()!;
+
+ return (
+
+
+
{props.label + ":"}
+
+
+
+ );
+};
diff --git a/examples/07-collaboration/05-comments/src/style.css b/examples/07-collaboration/05-comments/src/style.css
new file mode 100644
index 0000000000..eaf9d337e9
--- /dev/null
+++ b/examples/07-collaboration/05-comments/src/style.css
@@ -0,0 +1,47 @@
+.comments-main-container {
+ align-items: center;
+ background-color: var(--bn-colors-disabled-background);
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 10px;
+ height: 100%;
+ max-width: none;
+ padding: 10px;
+ width: 100%;
+}
+
+.comments-main-container .bn-editor {
+ height: 100%;
+ max-width: 700px;
+ overflow: auto;
+ width: 100%;
+}
+
+.comments-main-container .settings {
+ display: flex;
+ max-width: 700px;
+ width: 100%;
+}
+
+.comments-main-container .settings-select {
+ display: flex;
+ gap: 10px;
+}
+
+.comments-main-container .settings-select .bn-toolbar {
+ align-items: center;
+ box-shadow: none;
+}
+
+.comments-main-container .settings-select h2 {
+ color: var(--bn-colors-menu-text);
+ margin: 0;
+ font-size: 12px;
+ line-height: 12px;
+ padding-left: 14px;
+}
+
+.bn-thread {
+ max-height: 200px;
+ overflow: auto !important;
+}
diff --git a/examples/07-collaboration/05-comments/src/userdata.ts b/examples/07-collaboration/05-comments/src/userdata.ts
new file mode 100644
index 0000000000..c54eaf0f9a
--- /dev/null
+++ b/examples/07-collaboration/05-comments/src/userdata.ts
@@ -0,0 +1,47 @@
+import type { User } from "@blocknote/core/comments";
+
+const colors = [
+ "#958DF1",
+ "#F98181",
+ "#FBBC88",
+ "#FAF594",
+ "#70CFF8",
+ "#94FADB",
+ "#B9F18D",
+];
+
+const getRandomElement = (list: any[]) =>
+ list[Math.floor(Math.random() * list.length)];
+
+export const getRandomColor = () => getRandomElement(colors);
+
+export type MyUserType = User & {
+ role: "editor" | "comment";
+};
+
+export const HARDCODED_USERS: MyUserType[] = [
+ {
+ id: "1",
+ username: "John Doe",
+ avatarUrl: "https://placehold.co/100x100?text=John",
+ role: "editor",
+ },
+ {
+ id: "2",
+ username: "Jane Doe",
+ avatarUrl: "https://placehold.co/100x100?text=Jane",
+ role: "editor",
+ },
+ {
+ id: "3",
+ username: "Bob Smith",
+ avatarUrl: "https://placehold.co/100x100?text=Bob",
+ role: "comment",
+ },
+ {
+ id: "4",
+ username: "Betty Smith",
+ avatarUrl: "https://placehold.co/100x100?text=Betty",
+ role: "comment",
+ },
+];
diff --git a/examples/07-collaboration/05-comments/tsconfig.json b/examples/07-collaboration/05-comments/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/05-comments/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/05-comments/vite.config.ts b/examples/07-collaboration/05-comments/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/05-comments/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json b/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json
new file mode 100644
index 0000000000..ff82fe290f
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["Advanced", "Comments", "Collaboration"],
+ "dependencies": {
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27",
+ "@mantine/core": "^9.0.2"
+ }
+}
diff --git a/examples/07-collaboration/06-comments-with-sidebar/README.md b/examples/07-collaboration/06-comments-with-sidebar/README.md
new file mode 100644
index 0000000000..2502620fbe
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/README.md
@@ -0,0 +1,14 @@
+# Threads Sidebar
+
+In this example, you can add comments to the document while collaborating with others. You can also pick user accounts with different permissions, as well as react to, reply to, and resolve existing comments. The comments are displayed floating next to the text they refer to, and appear when selecting said text. The comments are shown in a separate sidebar using the `ThreadsSidebar` component.
+
+**Try it out:** Click the "Add comment" button in
+the [Formatting Toolbar](/docs/react/components/formatting-toolbar) to add a
+comment!
+
+**Relevant Docs:**
+
+- [Comments Sidebar](/docs/features/collaboration/comments#sidebar-view)
+- [Real-time collaboration](/docs/features/collaboration)
+- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/07-collaboration/06-comments-with-sidebar/index.html b/examples/07-collaboration/06-comments-with-sidebar/index.html
new file mode 100644
index 0000000000..6af0826869
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Threads Sidebar
+
+
+
+
+
+
+
diff --git a/examples/07-collaboration/06-comments-with-sidebar/main.tsx b/examples/07-collaboration/06-comments-with-sidebar/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/07-collaboration/06-comments-with-sidebar/package.json b/examples/07-collaboration/06-comments-with-sidebar/package.json
new file mode 100644
index 0000000000..67a5504590
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-collaboration-comments-with-sidebar",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx b/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx
new file mode 100644
index 0000000000..84ad0d577a
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx
@@ -0,0 +1,194 @@
+"use client";
+
+import {
+ DefaultThreadStoreAuth,
+ YjsThreadStore,
+ CommentsExtension,
+} from "@blocknote/core/comments";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ BlockNoteViewEditor,
+ FloatingComposerController,
+ ThreadsSidebar,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { useMemo, useState } from "react";
+import YPartyKitProvider from "y-partykit/provider";
+import * as Y from "yjs";
+
+import { SettingsSelect } from "./SettingsSelect";
+import { HARDCODED_USERS, MyUserType, getRandomColor } from "./userdata";
+
+import "./style.css";
+
+// The resolveUsers function fetches information about your users
+// (e.g. their name, avatar, etc.). Usually, you'd fetch this from your
+// own database or user management system.
+// Here, we just return the hardcoded users (from userdata.ts)
+async function resolveUsers(userIds: string[]) {
+ // fake a (slow) network request
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
+}
+
+// Sets up Yjs document and PartyKit Yjs provider.
+const doc = new Y.Doc();
+const provider = new YPartyKitProvider(
+ "blocknote-dev.yousefed.partykit.dev",
+ // Use a unique name as a "room" for your application.
+ "comments-with-sidebar",
+ doc,
+);
+
+// This follows the Y-Sweet example to setup a collabotive editor
+// (but of course, you also use other collaboration providers
+// see the docs for more information)
+export default function App() {
+ const [activeUser, setActiveUser] = useState(HARDCODED_USERS[0]);
+ const [commentFilter, setCommentFilter] = useState<
+ "open" | "resolved" | "all"
+ >("open");
+ const [commentSort, setCommentSort] = useState<
+ "position" | "recent-activity" | "oldest"
+ >("position");
+
+ // setup the thread store which stores / and syncs thread / comment data
+ const threadStore = useMemo(() => {
+ // (alternative, use TiptapCollabProvider)
+ // const provider = new TiptapCollabProvider({
+ // name: "test",
+ // baseUrl: "https://collab.yourdomain.com",
+ // appId: "test",
+ // document: doc,
+ // });
+ // return new TiptapThreadStore(
+ // activeUser.id,
+ // provider,
+ // new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
+ // );
+ return new YjsThreadStore(
+ activeUser.id,
+ doc.getMap("threads"),
+ new DefaultThreadStoreAuth(activeUser.id, activeUser.role),
+ );
+ }, [doc, activeUser]);
+
+ // setup the editor with comments and collaboration
+ const editor = useCreateBlockNote(
+ {
+ collaboration: {
+ provider,
+ fragment: doc.getXmlFragment("blocknote"),
+ user: { color: getRandomColor(), name: activeUser.username },
+ },
+ extensions: [CommentsExtension({ threadStore, resolveUsers })],
+ },
+ [activeUser, threadStore],
+ );
+
+ return (
+
+ {/* We place the editor, the sidebar, and any settings selects within
+ `BlockNoteView` as they use BlockNote UI components and need the context
+ for them. */}
+
+ {/* Because we set `renderEditor` to false, we can now manually place
+ `BlockNoteViewEditor` (the actual editor component) in its own
+ section below the user settings select. */}
+
+ {/* Since we disabled rendering of comments with `comments={false}`,
+ we need to re-add the floating composer, which is the UI element that
+ appears when creating new threads. */}
+
+
+
+ {/* We also place the `ThreadsSidebar` component in its own section,
+ along with settings for filtering and sorting. */}
+
+
+ );
+}
diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/SettingsSelect.tsx b/examples/07-collaboration/06-comments-with-sidebar/src/SettingsSelect.tsx
new file mode 100644
index 0000000000..0dfc79dc3f
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/src/SettingsSelect.tsx
@@ -0,0 +1,24 @@
+import { ComponentProps, useComponentsContext } from "@blocknote/react";
+
+// This component is used to display a selection dropdown with a label. By using
+// the useComponentsContext hook, we can create it out of existing components
+// within the same UI library that `BlockNoteView` uses (Mantine, Ariakit, or
+// ShadCN), to match the design of the editor.
+export const SettingsSelect = (props: {
+ label: string;
+ items: ComponentProps["FormattingToolbar"]["Select"]["items"];
+}) => {
+ const Components = useComponentsContext()!;
+
+ return (
+
+
+
{props.label + ":"}
+
+
+
+ );
+};
diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/style.css b/examples/07-collaboration/06-comments-with-sidebar/src/style.css
new file mode 100644
index 0000000000..f903d52e1b
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/src/style.css
@@ -0,0 +1,73 @@
+.sidebar-comments-main-container {
+ background-color: var(--bn-colors-disabled-background);
+ display: flex;
+ gap: 10px;
+ height: 100%;
+ max-width: none;
+ padding: 10px;
+ width: 100%;
+}
+
+.sidebar-comments-main-container .editor-layout-wrapper {
+ display: flex;
+ flex: 2;
+ justify-content: center;
+ width: 0;
+}
+
+.sidebar-comments-main-container .editor-section,
+.threads-sidebar-section {
+ border-radius: var(--bn-border-radius-large);
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: 10px;
+ max-height: 100%;
+ min-width: 350px;
+ width: 0;
+}
+
+.sidebar-comments-main-container .editor-section h1,
+.threads-sidebar-section h1 {
+ color: var(--bn-colors-menu-text);
+ margin: 0;
+ font-size: 32px;
+}
+
+.sidebar-comments-main-container .bn-editor,
+.bn-threads-sidebar {
+ border-radius: var(--bn-border-radius-medium);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ height: 100%;
+ overflow: auto;
+}
+
+.sidebar-comments-main-container .editor-section {
+ max-width: 700px;
+}
+
+.sidebar-comments-main-container .settings {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.sidebar-comments-main-container .settings-select {
+ display: flex;
+ gap: 10px;
+}
+
+.sidebar-comments-main-container .settings-select .bn-toolbar {
+ align-items: center;
+ box-shadow: none;
+}
+
+.sidebar-comments-main-container .settings-select h2 {
+ color: var(--bn-colors-menu-text);
+ margin: 0;
+ font-size: 12px;
+ line-height: 12px;
+ padding-left: 14px;
+}
diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts b/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts
new file mode 100644
index 0000000000..c54eaf0f9a
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts
@@ -0,0 +1,47 @@
+import type { User } from "@blocknote/core/comments";
+
+const colors = [
+ "#958DF1",
+ "#F98181",
+ "#FBBC88",
+ "#FAF594",
+ "#70CFF8",
+ "#94FADB",
+ "#B9F18D",
+];
+
+const getRandomElement = (list: any[]) =>
+ list[Math.floor(Math.random() * list.length)];
+
+export const getRandomColor = () => getRandomElement(colors);
+
+export type MyUserType = User & {
+ role: "editor" | "comment";
+};
+
+export const HARDCODED_USERS: MyUserType[] = [
+ {
+ id: "1",
+ username: "John Doe",
+ avatarUrl: "https://placehold.co/100x100?text=John",
+ role: "editor",
+ },
+ {
+ id: "2",
+ username: "Jane Doe",
+ avatarUrl: "https://placehold.co/100x100?text=Jane",
+ role: "editor",
+ },
+ {
+ id: "3",
+ username: "Bob Smith",
+ avatarUrl: "https://placehold.co/100x100?text=Bob",
+ role: "comment",
+ },
+ {
+ id: "4",
+ username: "Betty Smith",
+ avatarUrl: "https://placehold.co/100x100?text=Betty",
+ role: "comment",
+ },
+];
diff --git a/examples/07-collaboration/06-comments-with-sidebar/tsconfig.json b/examples/07-collaboration/06-comments-with-sidebar/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/06-comments-with-sidebar/vite.config.ts b/examples/07-collaboration/06-comments-with-sidebar/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/06-comments-with-sidebar/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/07-collaboration/07-ghost-writer/.bnexample.json b/examples/07-collaboration/07-ghost-writer/.bnexample.json
new file mode 100644
index 0000000000..2c30ef42bd
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/.bnexample.json
@@ -0,0 +1,10 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "nperez0111",
+ "tags": ["Advanced", "Development", "Collaboration"],
+ "dependencies": {
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ }
+}
diff --git a/examples/07-collaboration/07-ghost-writer/README.md b/examples/07-collaboration/07-ghost-writer/README.md
new file mode 100644
index 0000000000..608251baeb
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/README.md
@@ -0,0 +1,9 @@
+# Ghost Writer
+
+In this example, we use a local Yjs document to store the document state, and have a ghost writer that edits the document in real-time.
+
+**Try it out:** Open this page in a new browser tab or window to see it in action!
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/07-collaboration/07-ghost-writer/index.html b/examples/07-collaboration/07-ghost-writer/index.html
new file mode 100644
index 0000000000..784f0cc6cd
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Ghost Writer
+
+
+
+
+
+
+
diff --git a/examples/07-collaboration/07-ghost-writer/main.tsx b/examples/07-collaboration/07-ghost-writer/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/07-collaboration/07-ghost-writer/package.json b/examples/07-collaboration/07-ghost-writer/package.json
new file mode 100644
index 0000000000..26e4956fab
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-collaboration-ghost-writer",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/07-ghost-writer/src/App.tsx b/examples/07-collaboration/07-ghost-writer/src/App.tsx
new file mode 100644
index 0000000000..4344c5c11a
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/src/App.tsx
@@ -0,0 +1,126 @@
+import "@blocknote/core/fonts/inter.css";
+import "@blocknote/mantine/style.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import { useCreateBlockNote } from "@blocknote/react";
+
+import YPartyKitProvider from "y-partykit/provider";
+import * as Y from "yjs";
+import "./styles.css";
+import { useEffect, useState } from "react";
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { EditorView } from "prosemirror-view";
+
+const params = new URLSearchParams(window.location.search);
+const ghostWritingRoom = params.get("room");
+const ghostWriterIndex = parseInt(params.get("index") || "1");
+const isGhostWriting = Boolean(ghostWritingRoom);
+const roomName = ghostWritingRoom || `ghost-writer-${Date.now()}`;
+// Sets up Yjs document and PartyKit Yjs provider.
+const doc = new Y.Doc();
+const provider = new YPartyKitProvider(
+ "blocknote-dev.yousefed.partykit.dev",
+ // Use a unique name as a "room" for your application.
+ roomName,
+ doc,
+);
+
+/**
+ * Y-prosemirror has an optimization, where it doesn't send awareness updates unless the editor is currently focused.
+ * So, for the ghost writers, we override the hasFocus method to always return true.
+ */
+if (isGhostWriting) {
+ EditorView.prototype.hasFocus = () => true;
+}
+
+const ghostContent =
+ "This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. ";
+
+export default function App() {
+ const [numGhostWriters, setNumGhostWriters] = useState(1);
+ const [isPaused, setIsPaused] = useState(false);
+ const editor = useCreateBlockNote({
+ collaboration: {
+ // The Yjs Provider responsible for transporting updates:
+ provider,
+ // Where to store BlockNote data in the Y.Doc:
+ fragment: doc.getXmlFragment("document-store"),
+ // Information (name and color) for this user:
+ user: {
+ name: isGhostWriting
+ ? `Ghost Writer #${ghostWriterIndex}`
+ : "My Username",
+ color: isGhostWriting ? "#CCCCCC" : "#00ff00",
+ },
+ },
+ });
+
+ useEffect(() => {
+ if (!isGhostWriting || isPaused) {
+ return;
+ }
+ let index = 0;
+ let timeout: NodeJS.Timeout;
+
+ const scheduleNextChar = () => {
+ const jitter = Math.random() * 200; // Random delay between 0-200ms
+ timeout = setTimeout(() => {
+ const firstBlock = editor.document?.[0];
+ if (firstBlock) {
+ editor.insertInlineContent(ghostContent[index], {
+ updateSelection: true,
+ });
+ index = (index + 1) % ghostContent.length;
+ }
+ scheduleNextChar();
+ }, 50 + jitter);
+ };
+
+ scheduleNextChar();
+
+ return () => clearTimeout(timeout);
+ }, [editor, isPaused]);
+
+ // Renders the editor instance.
+ return (
+ <>
+ {isGhostWriting ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+
+ {!isGhostWriting && (
+
+ )}
+ >
+ );
+}
diff --git a/examples/07-collaboration/07-ghost-writer/src/styles.css b/examples/07-collaboration/07-ghost-writer/src/styles.css
new file mode 100644
index 0000000000..588b4f01fa
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/src/styles.css
@@ -0,0 +1,12 @@
+.two-way-sync {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ margin-top: 10px;
+ gap: 8px;
+}
+
+.ghost-writer {
+ flex: 1;
+ border: 1px solid #ccc;
+}
diff --git a/examples/07-collaboration/07-ghost-writer/tsconfig.json b/examples/07-collaboration/07-ghost-writer/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/07-ghost-writer/vite.config.ts b/examples/07-collaboration/07-ghost-writer/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/07-ghost-writer/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/07-collaboration/08-forking/.bnexample.json b/examples/07-collaboration/08-forking/.bnexample.json
new file mode 100644
index 0000000000..2c30ef42bd
--- /dev/null
+++ b/examples/07-collaboration/08-forking/.bnexample.json
@@ -0,0 +1,10 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "nperez0111",
+ "tags": ["Advanced", "Development", "Collaboration"],
+ "dependencies": {
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ }
+}
diff --git a/examples/07-collaboration/08-forking/README.md b/examples/07-collaboration/08-forking/README.md
new file mode 100644
index 0000000000..997832dc16
--- /dev/null
+++ b/examples/07-collaboration/08-forking/README.md
@@ -0,0 +1,9 @@
+# Collaborative Editing with Forking
+
+In this example, we can fork a document and edit it independently of other collaborators. Then, we can choose to merge the changes back into the original document, or discard the changes.
+
+**Try it out:** Open this page in a new browser tab or window to see it in action!
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/07-collaboration/08-forking/index.html b/examples/07-collaboration/08-forking/index.html
new file mode 100644
index 0000000000..45ad926703
--- /dev/null
+++ b/examples/07-collaboration/08-forking/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Collaborative Editing with Forking
+
+
+
+
+
+
+
diff --git a/examples/07-collaboration/08-forking/main.tsx b/examples/07-collaboration/08-forking/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/07-collaboration/08-forking/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/07-collaboration/08-forking/package.json b/examples/07-collaboration/08-forking/package.json
new file mode 100644
index 0000000000..3d82fc59ba
--- /dev/null
+++ b/examples/07-collaboration/08-forking/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-collaboration-forking",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/08-forking/src/App.tsx b/examples/07-collaboration/08-forking/src/App.tsx
new file mode 100644
index 0000000000..d338e133d7
--- /dev/null
+++ b/examples/07-collaboration/08-forking/src/App.tsx
@@ -0,0 +1,76 @@
+import "@blocknote/core/fonts/inter.css";
+import {} from "@blocknote/core";
+import { ForkYDocExtension } from "@blocknote/core/extensions";
+import {
+ useCreateBlockNote,
+ useExtension,
+ useExtensionState,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import YPartyKitProvider from "y-partykit/provider";
+import * as Y from "yjs";
+
+// Sets up Yjs document and PartyKit Yjs provider.
+const doc = new Y.Doc();
+const provider = new YPartyKitProvider(
+ "blocknote-dev.yousefed.partykit.dev",
+ // Use a unique name as a "room" for your application.
+ "your-project-name-room",
+ doc,
+);
+
+export default function App() {
+ const editor = useCreateBlockNote({
+ collaboration: {
+ // The Yjs Provider responsible for transporting updates:
+ provider,
+ // Where to store BlockNote data in the Y.Doc:
+ fragment: doc.getXmlFragment("document-store"),
+ // Information (name and color) for this user:
+ user: {
+ name: "My Username",
+ color: "#ff0000",
+ },
+ },
+ });
+ const forkYDocPlugin = useExtension(ForkYDocExtension, { editor });
+ const isForked = useExtensionState(ForkYDocExtension, {
+ editor,
+ selector: (state) => state.isForked,
+ });
+
+ // Renders the editor instance.
+ return (
+ <>
+
+
+
+
+
Forked: {isForked ? "Yes" : "No"}
+
+
+ >
+ );
+}
diff --git a/examples/07-collaboration/08-forking/tsconfig.json b/examples/07-collaboration/08-forking/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/08-forking/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/08-forking/vite.config.ts b/examples/07-collaboration/08-forking/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/08-forking/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/07-collaboration/09-comments-testing/.bnexample.json b/examples/07-collaboration/09-comments-testing/.bnexample.json
new file mode 100644
index 0000000000..5d7d986420
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/.bnexample.json
@@ -0,0 +1,9 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": ["Advanced", "Comments", "Testing"],
+ "dependencies": {
+ "yjs": "^13.6.27"
+ }
+}
diff --git a/examples/07-collaboration/09-comments-testing/README.md b/examples/07-collaboration/09-comments-testing/README.md
new file mode 100644
index 0000000000..b59f2ecd1b
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/README.md
@@ -0,0 +1,3 @@
+# Comments Testing
+
+A minimal comments example used for end-to-end testing. Uses a local Y.Doc (no collaboration provider) with a single hardcoded editor user.
diff --git a/examples/07-collaboration/09-comments-testing/index.html b/examples/07-collaboration/09-comments-testing/index.html
new file mode 100644
index 0000000000..f50976be79
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Comments Testing
+
+
+
+
+
+
+
diff --git a/examples/07-collaboration/09-comments-testing/main.tsx b/examples/07-collaboration/09-comments-testing/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/07-collaboration/09-comments-testing/package.json b/examples/07-collaboration/09-comments-testing/package.json
new file mode 100644
index 0000000000..c31e6c15c3
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-collaboration-comments-testing",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/09-comments-testing/src/App.tsx b/examples/07-collaboration/09-comments-testing/src/App.tsx
new file mode 100644
index 0000000000..3bada358c1
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/src/App.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import {
+ CommentsExtension,
+ DefaultThreadStoreAuth,
+ YjsThreadStore,
+} from "@blocknote/core/comments";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { useMemo } from "react";
+import * as Y from "yjs";
+
+const USER = {
+ id: "1",
+ username: "John Doe",
+ avatarUrl: "https://placehold.co/100x100?text=John",
+ role: "editor" as const,
+};
+
+async function resolveUsers(userIds: string[]) {
+ return [USER].filter((user) => userIds.includes(user.id));
+}
+
+export default function App() {
+ const doc = useMemo(() => new Y.Doc(), []);
+
+ const threadStore = useMemo(() => {
+ return new YjsThreadStore(
+ USER.id,
+ doc.getMap("threads"),
+ new DefaultThreadStoreAuth(USER.id, USER.role),
+ );
+ }, [doc]);
+
+ const editor = useCreateBlockNote(
+ {
+ extensions: [CommentsExtension({ threadStore, resolveUsers })],
+ },
+ [threadStore],
+ );
+
+ return ;
+}
diff --git a/examples/07-collaboration/09-comments-testing/tsconfig.json b/examples/07-collaboration/09-comments-testing/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/07-collaboration/09-comments-testing/vite.config.ts b/examples/07-collaboration/09-comments-testing/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/07-collaboration/09-comments-testing/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/.bnexample.json b/examples/08-extensions/01-tiptap-arrow-conversion/.bnexample.json
new file mode 100644
index 0000000000..1893df45b9
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/.bnexample.json
@@ -0,0 +1,10 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "komsenapati",
+ "tags": ["Extension"],
+ "pro": true,
+ "dependencies": {
+ "@tiptap/core": "^3.13.0"
+ }
+}
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/README.md b/examples/08-extensions/01-tiptap-arrow-conversion/README.md
new file mode 100644
index 0000000000..83175f3aab
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/README.md
@@ -0,0 +1,5 @@
+# TipTap extension (arrow InputRule)
+
+This example shows how to set up a BlockNote editor with a TipTap extension that registers an InputRule to convert `->` into `→`.
+
+**Try it out:** Type `->` anywhere in the editor and see how it's automatically converted to a single arrow unicode character.
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/index.html b/examples/08-extensions/01-tiptap-arrow-conversion/index.html
new file mode 100644
index 0000000000..1d1f24c59c
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ TipTap extension (arrow InputRule)
+
+
+
+
+
+
+
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/main.tsx b/examples/08-extensions/01-tiptap-arrow-conversion/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/package.json b/examples/08-extensions/01-tiptap-arrow-conversion/package.json
new file mode 100644
index 0000000000..c781441abe
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-extensions-tiptap-arrow-conversion",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@tiptap/core": "^3.13.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/src/App.tsx b/examples/08-extensions/01-tiptap-arrow-conversion/src/App.tsx
new file mode 100644
index 0000000000..3fbe199ec4
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/src/App.tsx
@@ -0,0 +1,18 @@
+import "@blocknote/core/fonts/inter.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import { ArrowConversionExtension } from "./ArrowConversionExtension";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ _tiptapOptions: {
+ extensions: [ArrowConversionExtension],
+ },
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/src/ArrowConversionExtension.ts b/examples/08-extensions/01-tiptap-arrow-conversion/src/ArrowConversionExtension.ts
new file mode 100644
index 0000000000..3adcef5653
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/src/ArrowConversionExtension.ts
@@ -0,0 +1,19 @@
+import { Extension } from "@tiptap/core";
+
+export const ArrowConversionExtension = Extension.create({
+ name: "arrowConversion",
+
+ addInputRules() {
+ return [
+ {
+ undoable: true,
+ find: /->/g,
+ handler: ({ state, range, chain }) => {
+ const { from, to } = range;
+ const tr = state.tr.replaceWith(from, to, state.schema.text("→"));
+ chain().insertContent(tr).run();
+ },
+ },
+ ];
+ },
+});
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/tsconfig.json b/examples/08-extensions/01-tiptap-arrow-conversion/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/08-extensions/01-tiptap-arrow-conversion/vite.config.ts b/examples/08-extensions/01-tiptap-arrow-conversion/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/08-extensions/01-tiptap-arrow-conversion/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/01-minimal/.bnexample.json b/examples/09-ai/01-minimal/.bnexample.json
new file mode 100644
index 0000000000..9aede450f7
--- /dev/null
+++ b/examples/09-ai/01-minimal/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5"
+ }
+}
diff --git a/examples/09-ai/01-minimal/README.md b/examples/09-ai/01-minimal/README.md
new file mode 100644
index 0000000000..5ffcb1af89
--- /dev/null
+++ b/examples/09-ai/01-minimal/README.md
@@ -0,0 +1,11 @@
+# Rich Text editor AI integration
+
+This example shows the minimal setup to add AI integration to your BlockNote rich text editor.
+
+Select some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.
+
+**Relevant Docs:**
+
+- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)
+- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)
+- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)
diff --git a/examples/09-ai/01-minimal/index.html b/examples/09-ai/01-minimal/index.html
new file mode 100644
index 0000000000..b31f43f604
--- /dev/null
+++ b/examples/09-ai/01-minimal/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Rich Text editor AI integration
+
+
+
+
+
+
+
diff --git a/examples/09-ai/01-minimal/main.tsx b/examples/09-ai/01-minimal/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/01-minimal/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/01-minimal/package.json b/examples/09-ai/01-minimal/package.json
new file mode 100644
index 0000000000..b4ef7599fc
--- /dev/null
+++ b/examples/09-ai/01-minimal/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-ai-minimal",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/01-minimal/src/App.tsx b/examples/09-ai/01-minimal/src/App.tsx
new file mode 100644
index 0000000000..3ada0eea97
--- /dev/null
+++ b/examples/09-ai/01-minimal/src/App.tsx
@@ -0,0 +1,121 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenuController,
+ AIToolbarButton,
+ getAISlashMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+
+import { DefaultChatTransport } from "ai";
+import { getEnv } from "./getEnv";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+ {/* Add the AI button */}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ // add the default AI slash menu items, or define your own
+ ...getAISlashMenuItems(editor),
+];
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ transport: new DefaultChatTransport({
+ // URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
+ api: `${BASE_URL}/regular/streamText`,
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ );
+}
diff --git a/examples/09-ai/01-minimal/src/getEnv.ts b/examples/09-ai/01-minimal/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/01-minimal/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/01-minimal/tsconfig.json b/examples/09-ai/01-minimal/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/01-minimal/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/01-minimal/vite.config.ts b/examples/09-ai/01-minimal/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/01-minimal/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/02-playground/.bnexample.json b/examples/09-ai/02-playground/.bnexample.json
new file mode 100644
index 0000000000..9aede450f7
--- /dev/null
+++ b/examples/09-ai/02-playground/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5"
+ }
+}
diff --git a/examples/09-ai/02-playground/README.md b/examples/09-ai/02-playground/README.md
new file mode 100644
index 0000000000..9a40ee10da
--- /dev/null
+++ b/examples/09-ai/02-playground/README.md
@@ -0,0 +1,12 @@
+# AI Playground
+
+Explore different LLM models integrated with BlockNote in the AI Playground.
+
+Change the configuration, then highlight some text to access the AI menu, or type `/ai` anywhere in the editor.
+
+**Relevant Docs:**
+
+- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)
+- [BlockNote AI Reference](/docs/features/ai/reference)
+- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar)
+- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)
diff --git a/examples/09-ai/02-playground/index.html b/examples/09-ai/02-playground/index.html
new file mode 100644
index 0000000000..94be2156bf
--- /dev/null
+++ b/examples/09-ai/02-playground/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ AI Playground
+
+
+
+
+
+
+
diff --git a/examples/09-ai/02-playground/main.tsx b/examples/09-ai/02-playground/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/02-playground/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/02-playground/package.json b/examples/09-ai/02-playground/package.json
new file mode 100644
index 0000000000..98f9c40219
--- /dev/null
+++ b/examples/09-ai/02-playground/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-ai-playground",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/02-playground/src/App.tsx b/examples/09-ai/02-playground/src/App.tsx
new file mode 100644
index 0000000000..008b17b40a
--- /dev/null
+++ b/examples/09-ai/02-playground/src/App.tsx
@@ -0,0 +1,165 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ useBlockNoteContext,
+ useCreateBlockNote,
+ useExtension,
+ usePrefersColorScheme,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenuController,
+ AIToolbarButton,
+ getAISlashMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+import { Fieldset, MantineProvider } from "@mantine/core";
+import { useEffect, useState } from "react";
+
+import { DefaultChatTransport } from "ai";
+import { BasicAutocomplete } from "./AutoComplete";
+import { getEnv } from "./getEnv";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ ...getAISlashMenuItems(editor),
+];
+
+export default function App() {
+ const [model, setModel] = useState(
+ "groq.chat/llama-3.3-70b-versatile",
+ );
+
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ transport: new DefaultChatTransport({
+ // URL to your backend API, see example source in `packages/xl-ai-server/src/routes/regular.ts`
+ api: `${BASE_URL}/model-playground/streamText`,
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ const ai = useExtension(AIExtension, { editor });
+
+ useEffect(() => {
+ // update the default model in the extension
+
+ // Add the model string to the request body
+ ai.options.setState({
+ ...ai.options.state,
+ chatRequestOptions: {
+ body: {
+ model,
+ },
+ },
+ });
+ }, [model, ai]);
+
+ const themePreference = usePrefersColorScheme();
+ const existingContext = useBlockNoteContext();
+
+ const theme =
+ existingContext?.colorSchemePreference ||
+ (themePreference === "no-preference" ? "light" : themePreference);
+
+ return (
+
+ undefined}
+ >
+
+
+
+
+
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ //
+ //
+ ))}
+
+
+);
+
+export default RadioGroupComponent;
diff --git a/examples/09-ai/02-playground/src/data/aimodels.ts b/examples/09-ai/02-playground/src/data/aimodels.ts
new file mode 100644
index 0000000000..329805d1b2
--- /dev/null
+++ b/examples/09-ai/02-playground/src/data/aimodels.ts
@@ -0,0 +1,23 @@
+export const AI_MODELS = [
+ "openai.chat/gpt-4o",
+ "openai.chat/gpt-4o-mini",
+ "openai.chat/gpt-4.1",
+ "groq.chat/llama-3.3-70b-versatile",
+ "groq.chat/llama-3.1-8b-instant",
+ "groq.chat/llama3-70b-8192",
+ "groq.chat/deepseek-r1-distill-llama-70b",
+ "groq.chat/qwen-qwq-32b",
+ "mistral.chat/mistral-large-latest",
+ "mistral.chat/mistral-medium-latest",
+ "mistral.chat/ministral-3b-latest",
+ "mistral.chat/ministral-8b-latest",
+ "anthropic.chat/claude-opus-4-5",
+ "anthropic.chat/claude-sonnet-4-5",
+ "anthropic.chat/claude-3-7-sonnet-latest",
+ "anthropic.chat/claude-3-5-haiku-latest",
+ "albert-etalab.chat/albert-large",
+ "google.generative-ai/gemini-1.5-pro",
+ "google.generative-ai/gemini-1.5-flash",
+ "google.generative-ai/gemini-2.5-pro",
+ "google.generative-ai/gemini-2.5-flash",
+];
diff --git a/examples/09-ai/02-playground/src/getEnv.ts b/examples/09-ai/02-playground/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/02-playground/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/02-playground/tsconfig.json b/examples/09-ai/02-playground/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/02-playground/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/02-playground/vite.config.ts b/examples/09-ai/02-playground/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/02-playground/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/03-custom-ai-menu-items/.bnexample.json b/examples/09-ai/03-custom-ai-menu-items/.bnexample.json
new file mode 100644
index 0000000000..9a91d82062
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5",
+ "react-icons": "^5.5.0"
+ }
+}
diff --git a/examples/09-ai/03-custom-ai-menu-items/README.md b/examples/09-ai/03-custom-ai-menu-items/README.md
new file mode 100644
index 0000000000..04240dda06
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/README.md
@@ -0,0 +1,10 @@
+# Adding AI Menu Items
+
+In this example, we add two items to the AI Menu. The first prompts the AI to make the selected text more casual, and can be found by selecting some text and click the AI (stars) button. The second prompts the AI to give ideas on related topics to extend the document with, and can be found by clicking the "Ask AI" Slash Menu item.
+
+Select some text and click the AI (stars) button, or type `/ai` anywhere in the editor to access AI functionality.
+
+**Relevant Docs:**
+
+- [Getting Stared with BlockNote AI](/docs/features/ai/getting-started)
+- [Custom AI Menu Items](/docs/features/ai/custom-commands)
diff --git a/examples/09-ai/03-custom-ai-menu-items/index.html b/examples/09-ai/03-custom-ai-menu-items/index.html
new file mode 100644
index 0000000000..478d198cf1
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Adding AI Menu Items
+
+
+
+
+
+
+
diff --git a/examples/09-ai/03-custom-ai-menu-items/main.tsx b/examples/09-ai/03-custom-ai-menu-items/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/03-custom-ai-menu-items/package.json b/examples/09-ai/03-custom-ai-menu-items/package.json
new file mode 100644
index 0000000000..ee4c5d2163
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@blocknote/example-ai-custom-ai-menu-items",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5",
+ "react-icons": "^5.5.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/03-custom-ai-menu-items/src/App.tsx b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx
new file mode 100644
index 0000000000..3646cc8d9e
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/src/App.tsx
@@ -0,0 +1,164 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenu,
+ AIMenuController,
+ AIToolbarButton,
+ getAISlashMenuItems,
+ getDefaultAIMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+import { getEnv } from "./getEnv";
+
+import { DefaultChatTransport } from "ai";
+import { addRelatedTopics, makeInformal } from "./customAIMenuItems";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+function CustomAIMenu() {
+ return (
+ ,
+ aiResponseStatus:
+ | "user-input"
+ | "thinking"
+ | "ai-writing"
+ | "error"
+ | "user-reviewing"
+ | "closed",
+ ) => {
+ if (aiResponseStatus === "user-input") {
+ // Returns different items based on whether the AI Menu was
+ // opened via the Formatting Toolbar or the Slash Menu.
+ if (editor.getSelection()) {
+ return [
+ // Gets the default AI Menu items
+ ...getDefaultAIMenuItems(editor, aiResponseStatus),
+ // Adds our custom item to make the text more casual.
+ // Only appears when the AI Menu is opened via the
+ // Formatting Toolbar.
+ makeInformal(editor),
+ ];
+ } else {
+ return [
+ // Gets the default AI Menu items
+ ...getDefaultAIMenuItems(editor, aiResponseStatus),
+ // Adds our custom item to find related topics. Only
+ // appears when the AI Menu is opened via the Slash
+ // Menu.
+ addRelatedTopics(editor),
+ ];
+ }
+ }
+ // for other states, return the default items
+ return getDefaultAIMenuItems(editor, aiResponseStatus);
+ }}
+ />
+ );
+}
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ ...getAISlashMenuItems(editor),
+];
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ transport: new DefaultChatTransport({
+ api: `${BASE_URL}/regular/streamText`,
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+ {/* Creates a new AIMenu with the default items,
+ as well as our custom ones. */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ );
+}
diff --git a/examples/09-ai/03-custom-ai-menu-items/src/customAIMenuItems.tsx b/examples/09-ai/03-custom-ai-menu-items/src/customAIMenuItems.tsx
new file mode 100644
index 0000000000..526589cdf2
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/src/customAIMenuItems.tsx
@@ -0,0 +1,71 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import {
+ AIExtension,
+ aiDocumentFormats,
+ AIMenuSuggestionItem,
+} from "@blocknote/xl-ai";
+import { RiApps2AddFill, RiEmotionHappyFill } from "react-icons/ri";
+
+// Custom item to make the text more informal.
+export const makeInformal = (
+ editor: BlockNoteEditor,
+): AIMenuSuggestionItem => ({
+ key: "make_informal",
+ title: "Make Informal",
+ // Aliases used when filtering AI Menu items from
+ // text in prompt input.
+ aliases: ["informal", "make informal", "casual"],
+ icon: ,
+ onItemClick: async () => {
+ await editor.getExtension(AIExtension)?.invokeAI({
+ userPrompt: "Give the selected text a more informal (casual) tone",
+ // Set to true to tell the LLM to specifically
+ // use the selected content as context. Defaults
+ // to false.
+ useSelection: true,
+ // Sets what operations the LLM is allowed to do.
+ // In this case, we only want to allow updating
+ // the selected content, so only `update` is set
+ // to true. Defaults to `true` for all
+ // operations.
+ streamToolsProvider: aiDocumentFormats.html.getStreamToolsProvider({
+ defaultStreamTools: {
+ add: false,
+ delete: false,
+ update: true,
+ },
+ }),
+ });
+ },
+ size: "small",
+});
+
+// Custom item to write about related topics.
+export const addRelatedTopics = (
+ editor: BlockNoteEditor,
+): AIMenuSuggestionItem => ({
+ key: "add_related_topics",
+ title: "Add Related Topics",
+ // Aliases used when filtering AI Menu items from
+ // text in prompt input.
+ aliases: ["related topics", "find topics"],
+ icon: ,
+ onItemClick: async () => {
+ await editor.getExtension(AIExtension)?.invokeAI({
+ userPrompt:
+ "Think of some related topics to the current text and write a sentence about each",
+ // Sets what operations the LLM is allowed to do.
+ // In this case, we only want to allow adding new
+ // content, so only `add` is set to true.
+ // Defaults to `true` for all operations.
+ streamToolsProvider: aiDocumentFormats.html.getStreamToolsProvider({
+ defaultStreamTools: {
+ add: true,
+ delete: false,
+ update: false,
+ },
+ }),
+ });
+ },
+ size: "small",
+});
diff --git a/examples/09-ai/03-custom-ai-menu-items/src/getEnv.ts b/examples/09-ai/03-custom-ai-menu-items/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/03-custom-ai-menu-items/tsconfig.json b/examples/09-ai/03-custom-ai-menu-items/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/03-custom-ai-menu-items/vite.config.ts b/examples/09-ai/03-custom-ai-menu-items/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/03-custom-ai-menu-items/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/04-with-collaboration/.bnexample.json b/examples/09-ai/04-with-collaboration/.bnexample.json
new file mode 100644
index 0000000000..83bed82fe4
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/.bnexample.json
@@ -0,0 +1,13 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "nperez0111",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ }
+}
diff --git a/examples/09-ai/04-with-collaboration/README.md b/examples/09-ai/04-with-collaboration/README.md
new file mode 100644
index 0000000000..6b46d05091
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/README.md
@@ -0,0 +1,10 @@
+# AI + Ghost Writer
+
+This example combines the AI extension with the ghost writer example to show how to use the AI extension in a collaborative environment.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
+- [Changing the Formatting Toolbar](/docs/react/components/formatting-toolbar#changing-the-formatting-toolbar)
+- [Changing Slash Menu Items](/docs/react/components/suggestion-menus#changing-slash-menu-items)
+- [Getting Stared with BlockNote AI](/docs/features/ai/setup)
diff --git a/examples/09-ai/04-with-collaboration/index.html b/examples/09-ai/04-with-collaboration/index.html
new file mode 100644
index 0000000000..fcd4ed14ca
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ AI + Ghost Writer
+
+
+
+
+
+
+
diff --git a/examples/09-ai/04-with-collaboration/main.tsx b/examples/09-ai/04-with-collaboration/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/04-with-collaboration/package.json b/examples/09-ai/04-with-collaboration/package.json
new file mode 100644
index 0000000000..e75864a84f
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@blocknote/example-ai-with-collaboration",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/04-with-collaboration/src/App.tsx b/examples/09-ai/04-with-collaboration/src/App.tsx
new file mode 100644
index 0000000000..9073141352
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/src/App.tsx
@@ -0,0 +1,228 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ SuggestionMenuController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenuController,
+ AIToolbarButton,
+ getAISlashMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+import { useEffect, useState } from "react";
+import YPartyKitProvider from "y-partykit/provider";
+import * as Y from "yjs";
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { EditorView } from "prosemirror-view";
+
+import { DefaultChatTransport } from "ai";
+import { getEnv } from "./getEnv";
+import "./styles.css";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+const params = new URLSearchParams(window.location.search);
+const ghostWritingRoom = params.get("room");
+const ghostWriterIndex = parseInt(params.get("index") || "1");
+const isGhostWriting = Boolean(ghostWritingRoom);
+const roomName = ghostWritingRoom || `ghost-writer-${Date.now()}`;
+// Sets up Yjs document and PartyKit Yjs provider.
+const doc = new Y.Doc();
+const provider = new YPartyKitProvider(
+ "blocknote-dev.yousefed.partykit.dev",
+ // Use a unique name as a "room" for your application.
+ roomName,
+ doc,
+);
+
+/**
+ * Y-prosemirror has an optimization, where it doesn't send awareness updates unless the editor is currently focused.
+ * So, for the ghost writers, we override the hasFocus method to always return true.
+ */
+if (isGhostWriting) {
+ EditorView.prototype.hasFocus = () => true;
+}
+
+const ghostContent =
+ "This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. ";
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+ {/* Add the AI button */}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ // add the default AI slash menu items, or define your own
+ ...getAISlashMenuItems(editor),
+];
+
+export default function App() {
+ const [numGhostWriters, setNumGhostWriters] = useState(1);
+ const [isPaused, setIsPaused] = useState(false);
+ const editor = useCreateBlockNote({
+ collaboration: {
+ // The Yjs Provider responsible for transporting updates:
+ provider,
+ // Where to store BlockNote data in the Y.Doc:
+ fragment: doc.getXmlFragment("document-store"),
+ // Information (name and color) for this user:
+ user: {
+ name: isGhostWriting
+ ? `Ghost Writer #${ghostWriterIndex}`
+ : "My Username",
+ color: isGhostWriting ? "#CCCCCC" : "#00ff00",
+ },
+ },
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ transport: new DefaultChatTransport({
+ api: `${BASE_URL}/regular/streamText`,
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ useEffect(() => {
+ if (!isGhostWriting || isPaused) {
+ return;
+ }
+ let index = 0;
+ let timeout: NodeJS.Timeout;
+
+ const scheduleNextChar = () => {
+ const jitter = Math.random() * 200; // Random delay between 0-200ms
+ timeout = setTimeout(() => {
+ const firstBlock = editor.document?.[0];
+ if (firstBlock) {
+ editor.insertInlineContent(ghostContent[index], {
+ updateSelection: true,
+ });
+ index = (index + 1) % ghostContent.length;
+ }
+ scheduleNextChar();
+ }, 50 + jitter);
+ };
+
+ scheduleNextChar();
+
+ return () => clearTimeout(timeout);
+ }, [editor, isPaused]);
+
+ // Renders the editor instance.
+ return (
+ <>
+ {isGhostWriting ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ {!isGhostWriting && (
+
+ )}
+ >
+ );
+}
diff --git a/examples/09-ai/04-with-collaboration/src/getEnv.ts b/examples/09-ai/04-with-collaboration/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/04-with-collaboration/src/styles.css b/examples/09-ai/04-with-collaboration/src/styles.css
new file mode 100644
index 0000000000..588b4f01fa
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/src/styles.css
@@ -0,0 +1,12 @@
+.two-way-sync {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ margin-top: 10px;
+ gap: 8px;
+}
+
+.ghost-writer {
+ flex: 1;
+ border: 1px solid #ccc;
+}
diff --git a/examples/09-ai/04-with-collaboration/tsconfig.json b/examples/09-ai/04-with-collaboration/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/04-with-collaboration/vite.config.ts b/examples/09-ai/04-with-collaboration/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/04-with-collaboration/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/05-manual-execution/.bnexample.json b/examples/09-ai/05-manual-execution/.bnexample.json
new file mode 100644
index 0000000000..890b2909fe
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/.bnexample.json
@@ -0,0 +1,13 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "yousefed",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ }
+}
diff --git a/examples/09-ai/05-manual-execution/README.md b/examples/09-ai/05-manual-execution/README.md
new file mode 100644
index 0000000000..74d066cc3e
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/README.md
@@ -0,0 +1,3 @@
+# AI manual execution
+
+Instead of calling AI models directly, this example shows how you can use an existing stream of responses and apply them to the editor.
diff --git a/examples/09-ai/05-manual-execution/index.html b/examples/09-ai/05-manual-execution/index.html
new file mode 100644
index 0000000000..c63d224da9
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ AI manual execution
+
+
+
+
+
+
+
diff --git a/examples/09-ai/05-manual-execution/main.tsx b/examples/09-ai/05-manual-execution/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/05-manual-execution/package.json b/examples/09-ai/05-manual-execution/package.json
new file mode 100644
index 0000000000..56ee0692fd
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@blocknote/example-ai-manual-execution",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5",
+ "y-partykit": "^0.0.25",
+ "yjs": "^13.6.27"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/05-manual-execution/src/App.tsx b/examples/09-ai/05-manual-execution/src/App.tsx
new file mode 100644
index 0000000000..9b10faef97
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/src/App.tsx
@@ -0,0 +1,197 @@
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+import {
+ AIExtension,
+ StreamToolExecutor,
+ aiDocumentFormats,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [AIExtension()],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+
+
+ {/*Inserts a new block at start of document.*/}
+
+
+
+
+
+ );
+}
diff --git a/examples/09-ai/05-manual-execution/src/getEnv.ts b/examples/09-ai/05-manual-execution/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/05-manual-execution/src/styles.css b/examples/09-ai/05-manual-execution/src/styles.css
new file mode 100644
index 0000000000..cc97b34a4f
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/src/styles.css
@@ -0,0 +1,15 @@
+.edit-buttons {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+
+.edit-button {
+ border: 1px solid gray;
+ border-radius: 4px;
+ padding-inline: 4px;
+}
+
+.edit-button:hover {
+ border: 1px solid lightgrey;
+}
diff --git a/examples/09-ai/05-manual-execution/tsconfig.json b/examples/09-ai/05-manual-execution/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/05-manual-execution/vite.config.ts b/examples/09-ai/05-manual-execution/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/05-manual-execution/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/06-client-side-transport/.bnexample.json b/examples/09-ai/06-client-side-transport/.bnexample.json
new file mode 100644
index 0000000000..0ac2b679fc
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@ai-sdk/groq": "^3.0.2",
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5"
+ }
+}
diff --git a/examples/09-ai/06-client-side-transport/README.md b/examples/09-ai/06-client-side-transport/README.md
new file mode 100644
index 0000000000..d4b4cc76c0
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/README.md
@@ -0,0 +1,5 @@
+# AI Integration with ClientSideTransport
+
+The standard setup is to have BlockNote AI call your server, which then calls an LLM of your choice. In this example, we show how you can use the `ClientSideTransport` to make calls directly to your LLM provider.
+
+To hide API keys of our LLM provider, we do still route calls through a proxy server using `fetchViaProxy` (this is optional).
diff --git a/examples/09-ai/06-client-side-transport/index.html b/examples/09-ai/06-client-side-transport/index.html
new file mode 100644
index 0000000000..0502452522
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ AI Integration with ClientSideTransport
+
+
+
+
+
+
+
diff --git a/examples/09-ai/06-client-side-transport/main.tsx b/examples/09-ai/06-client-side-transport/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/06-client-side-transport/package.json b/examples/09-ai/06-client-side-transport/package.json
new file mode 100644
index 0000000000..94250e9f3c
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@blocknote/example-ai-client-side-transport",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@ai-sdk/groq": "^3.0.2",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/06-client-side-transport/src/App.tsx b/examples/09-ai/06-client-side-transport/src/App.tsx
new file mode 100644
index 0000000000..14ed72abdb
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/src/App.tsx
@@ -0,0 +1,135 @@
+import { createGroq } from "@ai-sdk/groq";
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenuController,
+ AIToolbarButton,
+ ClientSideTransport,
+ fetchViaProxy,
+ getAISlashMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+import { getEnv } from "./getEnv";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+ {/* Add the AI button */}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ // add the default AI slash menu items, or define your own
+ ...getAISlashMenuItems(editor),
+];
+
+// We define the model directly in our app using the Vercel AI SDK
+const model = createGroq({
+ // We supply a custom fetch function so that requests are routed through our proxy server
+ // (see `packages/xl-ai-server/src/routes/proxy.ts`)
+ // this is needed to hide the API key of our LLM provider from the client,
+ // and to prevent CORS issues
+ fetch: fetchViaProxy(
+ (url) => `${BASE_URL}/proxy?provider=groq&url=${encodeURIComponent(url)}`,
+ ),
+ apiKey: "fake-api-key", // the API key is not used as it's actually added in the proxy server
+})("llama-3.3-70b-versatile");
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ // The ClientSideTransport is used so the client makes calls directly to `streamText`
+ // (whereas normally in the Vercel AI SDK, the client makes calls to your server, which then calls these methods)
+ // (see https://github.com/vercel/ai/issues/5140 for background info)
+ transport: new ClientSideTransport({
+ model,
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ );
+}
diff --git a/examples/09-ai/06-client-side-transport/src/getEnv.ts b/examples/09-ai/06-client-side-transport/src/getEnv.ts
new file mode 100644
index 0000000000..b225fc462e
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/src/getEnv.ts
@@ -0,0 +1,20 @@
+// helper function to get env variables across next / vite
+// only needed so this example works in BlockNote demos and docs
+export function getEnv(key: string) {
+ const env = (import.meta as any).env
+ ? {
+ BLOCKNOTE_AI_SERVER_API_KEY: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL: (import.meta as any).env
+ .VITE_BLOCKNOTE_AI_SERVER_BASE_URL,
+ }
+ : {
+ BLOCKNOTE_AI_SERVER_API_KEY:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY,
+ BLOCKNOTE_AI_SERVER_BASE_URL:
+ process.env.NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL,
+ };
+
+ const value = env[key as keyof typeof env];
+ return value;
+}
diff --git a/examples/09-ai/06-client-side-transport/tsconfig.json b/examples/09-ai/06-client-side-transport/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/09-ai/06-client-side-transport/vite.config.ts b/examples/09-ai/06-client-side-transport/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/09-ai/06-client-side-transport/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/examples/09-ai/07-server-persistence/.bnexample.json b/examples/09-ai/07-server-persistence/.bnexample.json
new file mode 100644
index 0000000000..12b79358ef
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "yousefed",
+ "tags": ["AI", "llm"],
+ "dependencies": {
+ "@blocknote/xl-ai": "latest",
+ "@mantine/core": "^9.0.2",
+ "ai": "^6.0.5"
+ }
+}
diff --git a/examples/09-ai/07-server-persistence/README.md b/examples/09-ai/07-server-persistence/README.md
new file mode 100644
index 0000000000..4dc00da84b
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/README.md
@@ -0,0 +1,5 @@
+# AI Integration with server LLM message persistence
+
+This example shows how to setup to add AI integration while handling the LLM calls (in this case, using the Vercel AI SDK) on your server, using a custom executor.
+
+Instead of sending all messages, these are kept server-side and we only submit the latest message.
diff --git a/examples/09-ai/07-server-persistence/index.html b/examples/09-ai/07-server-persistence/index.html
new file mode 100644
index 0000000000..edfab1aa18
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ AI Integration with server LLM message persistence
+
+
+
+
+
+
+
diff --git a/examples/09-ai/07-server-persistence/main.tsx b/examples/09-ai/07-server-persistence/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/09-ai/07-server-persistence/package.json b/examples/09-ai/07-server-persistence/package.json
new file mode 100644
index 0000000000..bbcb69e15d
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@blocknote/example-ai-server-persistence",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^9.0.2",
+ "@mantine/hooks": "^9.0.2",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "@blocknote/xl-ai": "latest",
+ "ai": "^6.0.5"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/09-ai/07-server-persistence/src/App.tsx b/examples/09-ai/07-server-persistence/src/App.tsx
new file mode 100644
index 0000000000..1d07beafac
--- /dev/null
+++ b/examples/09-ai/07-server-persistence/src/App.tsx
@@ -0,0 +1,150 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { en } from "@blocknote/core/locales";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ FormattingToolbar,
+ FormattingToolbarController,
+ getDefaultReactSlashMenuItems,
+ getFormattingToolbarItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import {
+ AIExtension,
+ AIMenuController,
+ AIToolbarButton,
+ getAISlashMenuItems,
+} from "@blocknote/xl-ai";
+import { en as aiEn } from "@blocknote/xl-ai/locales";
+import "@blocknote/xl-ai/style.css";
+import { DefaultChatTransport, isToolOrDynamicToolUIPart } from "ai";
+import { getEnv } from "./getEnv";
+
+const BASE_URL =
+ getEnv("BLOCKNOTE_AI_SERVER_BASE_URL") || "https://localhost:3000/ai";
+
+// Formatting toolbar with the `AIToolbarButton` added
+const FormattingToolbarWithAI = () => (
+
+ {...getFormattingToolbarItems()}
+ {/* Add the AI button */}
+
+
+);
+
+// Slash menu items with the AI option added
+const getSlashMenuItemsWithAI = (editor: BlockNoteEditor) => [
+ ...getDefaultReactSlashMenuItems(editor),
+ // add the default AI slash menu items, or define your own
+ ...getAISlashMenuItems(editor),
+];
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ dictionary: {
+ ...en,
+ ai: aiEn, // add default translations for the AI extension
+ },
+ // Register the AI extension
+ extensions: [
+ AIExtension({
+ // similar to https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#sending-only-the-last-message
+ // we adjust the transport to not send all messages to the backend
+ transport: new DefaultChatTransport({
+ // (see packages/xl-ai-server/src/routes/vercelAiSdkPersistence.ts)
+ api: `${BASE_URL}/server-persistence/streamText`,
+ prepareSendMessagesRequest({ id, body, messages, requestMetadata }) {
+ // we don't send the messages, just the information we need to compose / append messages server-side:
+ // - the conversation id
+ // - the new (last) message to send
+ // - the tool results of the last message
+
+ // we need to share data about tool calls with the backend,
+ // as these can be client-side executed. The backend needs to know the tool outputs
+ // in order to compose a new valid LLM request.
+ const lastToolParts =
+ messages.length > 1
+ ? messages[messages.length - 2].parts.filter((part) =>
+ isToolOrDynamicToolUIPart(part),
+ )
+ : [];
+
+ return {
+ body: {
+ ...body,
+ // TODO: this conversation id is client-side generated, we
+ // should have a server-side generated id to ensure uniqueness
+ // see https://github.com/vercel/ai/issues/7340#issuecomment-3307559636
+ id,
+ lastToolParts,
+ message: messages[messages.length - 1],
+ // messages, -> we explicitly don't send the messages array as we compose messages server-side
+ },
+ };
+ },
+ }),
+ }),
+ ],
+ // We set some initial content for demo purposes
+ initialContent: [
+ {
+ type: "heading",
+ props: {
+ level: 1,
+ },
+ content: "Open source software",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Open source software refers to computer programs whose source code is made available to the public, allowing anyone to view, modify, and distribute the code. This model stands in contrast to proprietary software, where the source code is kept secret and only the original creators have the right to make changes. Open projects are developed collaboratively, often by communities of developers from around the world, and are typically distributed under licenses that promote sharing and openness.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "One of the primary benefits of open source is the promotion of digital autonomy. By providing access to the source code, these programs empower users to control their own technology, customize software to fit their needs, and avoid vendor lock-in. This level of transparency also allows for greater security, as anyone can inspect the code for vulnerabilities or malicious elements. As a result, users are not solely dependent on a single company for updates, bug fixes, or continued support.",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Additionally, open development fosters innovation and collaboration. Developers can build upon existing projects, share improvements, and learn from each other, accelerating the pace of technological advancement. The open nature of these projects often leads to higher quality software, as bugs are identified and fixed more quickly by a diverse group of contributors. Furthermore, using open source can reduce costs for individuals, businesses, and governments, as it is often available for free and can be tailored to specific requirements without expensive licensing fees.",
+ },
+ ],
+ });
+
+ // Renders the editor instance using a React component.
+ return (
+
+
+ {/* Add the AI Command menu to the editor */}
+
+
+ {/* We disabled the default formatting toolbar with `formattingToolbar=false`
+ and replace it for one with an "AI button" (defined below).
+ (See "Formatting Toolbar" in docs)
+ */}
+
+
+ {/* We disabled the default SlashMenu with `slashMenu=false`
+ and replace it for one with an AI option (defined below).
+ (See "Suggestion Menus" in docs)
+ */}
+
+ filterSuggestionItems(getSlashMenuItemsWithAI(editor), query)
+ }
+ />
+
+
+ {/* Taken from Google Material Icons */}
+ {/* https://fonts.google.com/icons?selected=Material+Symbols+Rounded:progress_activity:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=load&icon.size=24&icon.color=%23e8eaed&icon.set=Material+Symbols&icon.style=Rounded&icon.platform=web */}
+
+
+ );
+});
diff --git a/packages/ariakit/src/tableHandle/ExtendButton.tsx b/packages/ariakit/src/tableHandle/ExtendButton.tsx
new file mode 100644
index 0000000000..f023837522
--- /dev/null
+++ b/packages/ariakit/src/tableHandle/ExtendButton.tsx
@@ -0,0 +1,31 @@
+import { Button as AriakitButton } from "@ariakit/react";
+
+import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const ExtendButton = forwardRef<
+ HTMLButtonElement,
+ ComponentProps["TableHandle"]["ExtendButton"]
+>((props, ref) => {
+ const { children, className, onMouseDown, onClick, ...rest } = props;
+
+ // false, because rest props can be added by mantine when button is used as a trigger
+ // assertEmpty in this case is only used at typescript level, not runtime level
+ assertEmpty(rest, false);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/ariakit/src/tableHandle/TableHandle.tsx b/packages/ariakit/src/tableHandle/TableHandle.tsx
new file mode 100644
index 0000000000..ace181fac4
--- /dev/null
+++ b/packages/ariakit/src/tableHandle/TableHandle.tsx
@@ -0,0 +1,43 @@
+import { Button as AriakitButton } from "@ariakit/react";
+
+import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const TableHandle = forwardRef<
+ HTMLButtonElement,
+ ComponentProps["TableHandle"]["Root"]
+>((props, ref) => {
+ const {
+ className,
+ children,
+ draggable,
+ onDragStart,
+ onDragEnd,
+ style,
+ label,
+ ...rest
+ } = props;
+
+ // false, because rest props can be added by ariakit when button is used as a trigger
+ // assertEmpty in this case is only used at typescript level, not runtime level
+ assertEmpty(rest, false);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/ariakit/src/toolbar/Toolbar.tsx b/packages/ariakit/src/toolbar/Toolbar.tsx
new file mode 100644
index 0000000000..0d2e38e345
--- /dev/null
+++ b/packages/ariakit/src/toolbar/Toolbar.tsx
@@ -0,0 +1,33 @@
+import { Toolbar as AriakitToolbar } from "@ariakit/react";
+
+import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+type ToolbarProps = ComponentProps["Generic"]["Toolbar"]["Root"];
+
+export const Toolbar = forwardRef(
+ (props, ref) => {
+ const {
+ className,
+ children,
+ onMouseEnter,
+ onMouseLeave,
+ variant,
+ ...rest
+ } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ {children}
+
+ );
+ },
+);
diff --git a/packages/ariakit/src/toolbar/ToolbarButton.tsx b/packages/ariakit/src/toolbar/ToolbarButton.tsx
new file mode 100644
index 0000000000..0fddcb5905
--- /dev/null
+++ b/packages/ariakit/src/toolbar/ToolbarButton.tsx
@@ -0,0 +1,73 @@
+import {
+ ToolbarItem as AriakitToolbarItem,
+ Tooltip as AriakitTooltip,
+ TooltipAnchor as AriakitTooltipAnchor,
+ TooltipProvider as AriakitTooltipProvider,
+} from "@ariakit/react";
+
+import { assertEmpty, isSafari, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+type ToolbarButtonProps = ComponentProps["Generic"]["Toolbar"]["Button"];
+
+/**
+ * Helper for basic buttons that show in the formatting toolbar.
+ */
+export const ToolbarButton = forwardRef(
+ (props, ref) => {
+ const {
+ className,
+ children,
+ mainTooltip,
+ secondaryTooltip,
+ icon,
+ isSelected,
+ isDisabled,
+ onClick,
+ label,
+ variant,
+ ...rest
+ } = props;
+
+ // false, because rest props can be added by ariakit when button is used as a trigger
+ // assertEmpty in this case is only used at typescript level, not runtime level
+ assertEmpty(rest, false);
+
+ return (
+
+ {
+ if (isSafari()) {
+ (e.currentTarget as HTMLButtonElement).focus();
+ }
+ }}
+ onClick={onClick}
+ aria-pressed={isSelected}
+ data-selected={isSelected ? "true" : undefined}
+ disabled={isDisabled || false}
+ ref={ref}
+ {...rest}
+ >
+ {icon}
+ {children}
+
+ }
+ />
+
+ {mainTooltip}
+ {secondaryTooltip && {secondaryTooltip}}
+
+
+ );
+ },
+);
diff --git a/packages/ariakit/src/toolbar/ToolbarSelect.tsx b/packages/ariakit/src/toolbar/ToolbarSelect.tsx
new file mode 100644
index 0000000000..f596cbbae6
--- /dev/null
+++ b/packages/ariakit/src/toolbar/ToolbarSelect.tsx
@@ -0,0 +1,58 @@
+import {
+ Select as AriakitSelect,
+ SelectArrow as AriakitSelectArrow,
+ SelectItem as AriakitSelectItem,
+ SelectItemCheck as AriakitSelectItemCheck,
+ SelectPopover as AriakitSelectPopover,
+ SelectProvider as AriakitSelectProvider,
+ ToolbarItem as AriakitToolbarItem,
+} from "@ariakit/react";
+
+import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const ToolbarSelect = forwardRef<
+ HTMLDivElement,
+ ComponentProps["FormattingToolbar"]["Select"]
+>((props, ref) => {
+ const { className, items, isDisabled, ...rest } = props;
+
+ assertEmpty(rest);
+
+ const selectedItem = props.items.filter((p) => p.isSelected)[0];
+
+ const setValue = (value: string) => {
+ items.find((item) => item.text === value)!.onClick?.();
+ };
+
+ return (
+
+ }
+ >
+ {selectedItem.icon} {selectedItem.text}
+
+
+ {items.map((option) => (
+
+ {option.icon}
+ {option.text}
+ {option.text === selectedItem.text && }
+
+ ))}
+
+
+ );
+});
diff --git a/packages/ariakit/tsconfig.json b/packages/ariakit/tsconfig.json
new file mode 100644
index 0000000000..2769eb7390
--- /dev/null
+++ b/packages/ariakit/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "jsx": "react-jsx",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "types",
+ "composite": true,
+ "skipLibCheck": true,
+ "emitDeclarationOnly": true
+ },
+ "include": ["src"],
+ "references": [
+ {
+ "path": "../core"
+ },
+ {
+ "path": "../react"
+ }
+ ]
+}
diff --git a/packages/ariakit/vite.config.ts b/packages/ariakit/vite.config.ts
new file mode 100644
index 0000000000..76bbb3f7b4
--- /dev/null
+++ b/packages/ariakit/vite.config.ts
@@ -0,0 +1,77 @@
+import react from "@vitejs/plugin-react";
+import * as path from "path";
+import { webpackStats } from "rollup-plugin-webpack-stats";
+import { defineConfig } from "vite";
+import pkg from "./package.json";
+// import eslintPlugin from "vite-plugin-eslint";
+
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ test: {
+ environment: "jsdom",
+ setupFiles: ["./vitestSetup.ts"],
+ },
+ plugins: [react(), webpackStats()],
+ // used so that vitest resolves the core package from the sources instead of the built version
+ resolve: {
+ alias:
+ conf.command === "build"
+ ? ({
+ // Vite 8's postcss-import can't resolve bare package specifiers in CSS @import
+ "@blocknote/react/style.css": path.resolve(
+ __dirname,
+ "../react/dist/style.css"
+ ),
+ } as Record)
+ : ({
+ // load live from sources with live reload working
+ "@blocknote/core": path.resolve(__dirname, "../core/src/"),
+ "@blocknote/react": path.resolve(__dirname, "../react/src/"),
+ } as Record),
+ },
+ build: {
+ sourcemap: true,
+ lib: {
+ entry: {
+ "blocknote-ariakit": path.resolve(__dirname, "src/index.tsx"),
+ },
+ name: "blocknote-ariakit",
+ cssFileName: "style",
+ formats: ["es", "cjs"],
+ fileName: (format, entryName) =>
+ format === "es" ? `${entryName}.js` : `${entryName}.cjs`,
+ },
+ rollupOptions: {
+ // make sure to externalize deps that shouldn't be bundled
+ // into your library
+ external: (source) => {
+ if (
+ Object.keys({
+ ...pkg.dependencies,
+ ...((pkg as any).peerDependencies || {}),
+ ...pkg.devDependencies,
+ }).some((dep) => source === dep || source.startsWith(dep + "/"))
+ ) {
+ return true;
+ }
+ return (
+ source.startsWith("react/") ||
+ source.startsWith("react-dom/") ||
+ source.startsWith("prosemirror-") ||
+ source.startsWith("@tiptap/") ||
+ source.startsWith("@blocknote/") ||
+ source.startsWith("@shikijs/") ||
+ source.startsWith("node:")
+ );
+ },
+ output: {
+ // Provide global variables to use in the UMD build
+ // for externalized deps
+ globals: {
+ react: "React",
+ "react-dom": "ReactDOM",
+ },
+ },
+ },
+ },
+}));
diff --git a/packages/code-block/.gitignore b/packages/code-block/.gitignore
new file mode 100644
index 0000000000..58f115c8dc
--- /dev/null
+++ b/packages/code-block/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-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/packages/code-block/LICENSE b/packages/code-block/LICENSE
new file mode 100644
index 0000000000..fa0086a952
--- /dev/null
+++ b/packages/code-block/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
\ No newline at end of file
diff --git a/packages/code-block/package.json b/packages/code-block/package.json
new file mode 100644
index 0000000000..f69342f942
--- /dev/null
+++ b/packages/code-block/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@blocknote/code-block",
+ "homepage": "https://github.com/TypeCellOS/BlockNote",
+ "private": false,
+ "sideEffects": false,
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TypeCellOS/BlockNote.git",
+ "directory": "packages/code-block"
+ },
+ "license": "MPL-2.0",
+ "version": "0.51.0",
+ "files": [
+ "dist",
+ "types",
+ "src"
+ ],
+ "keywords": [
+ "react",
+ "javascript",
+ "editor",
+ "typescript",
+ "prosemirror",
+ "wysiwyg",
+ "rich-text-editor",
+ "notion",
+ "yjs",
+ "block-based",
+ "tiptap"
+ ],
+ "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.",
+ "type": "module",
+ "source": "src/index.ts",
+ "types": "./types/src/index.d.ts",
+ "main": "./dist/blocknote-code-block.cjs",
+ "module": "./dist/blocknote-code-block.js",
+ "exports": {
+ ".": {
+ "types": "./types/src/index.d.ts",
+ "import": "./dist/blocknote-code-block.js",
+ "require": "./dist/blocknote-code-block.cjs"
+ }
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint src --max-warnings 0",
+ "test": "vitest --run",
+ "test-watch": "vitest watch"
+ },
+ "dependencies": {
+ "@blocknote/core": "0.51.0",
+ "@shikijs/core": "^4",
+ "@shikijs/engine-javascript": "^4",
+ "@shikijs/langs-precompiled": "^4",
+ "@shikijs/themes": "^4",
+ "@shikijs/types": "^4"
+ },
+ "devDependencies": {
+ "eslint": "^8.57.1",
+ "rollup-plugin-webpack-stats": "^0.2.6",
+ "typescript": "^5.9.3",
+ "vite": "^8.0.8",
+ "vite-plugin-eslint": "^1.8.1",
+ "vitest": "^4.1.2"
+ },
+ "peerDependencies": {
+ "@blocknote/core": "0.51.0"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../.eslintrc.json"
+ ]
+ },
+ "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d"
+}
diff --git a/packages/code-block/src/index.test.ts b/packages/code-block/src/index.test.ts
new file mode 100644
index 0000000000..f5dea4fd4a
--- /dev/null
+++ b/packages/code-block/src/index.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vitest";
+import { codeBlockOptions } from "./index.js";
+
+describe("codeBlock", () => {
+ it("should exist", () => {
+ expect(codeBlockOptions).toBeDefined();
+ });
+ it("should have defaultLanguage", () => {
+ expect(codeBlockOptions.defaultLanguage).toBeDefined();
+ });
+ it("should have supportedLanguages", () => {
+ expect(codeBlockOptions.supportedLanguages).toBeDefined();
+ });
+ it("should have createHighlighter", () => {
+ expect(codeBlockOptions.createHighlighter).toBeDefined();
+ });
+});
diff --git a/packages/code-block/src/index.ts b/packages/code-block/src/index.ts
new file mode 100644
index 0000000000..2cb588092d
--- /dev/null
+++ b/packages/code-block/src/index.ts
@@ -0,0 +1,205 @@
+import type { CodeBlockOptions } from "@blocknote/core";
+import { createHighlighter } from "./shiki.bundle.js";
+
+export const codeBlockOptions = {
+ defaultLanguage: "javascript",
+ supportedLanguages: {
+ text: {
+ name: "Plain Text",
+ aliases: ["text", "txt", "plain"],
+ },
+ c: {
+ name: "C",
+ aliases: ["c"],
+ },
+ cpp: {
+ name: "C++",
+ aliases: ["cpp", "c++"],
+ },
+ css: {
+ name: "CSS",
+ aliases: ["css"],
+ },
+ glsl: {
+ name: "GLSL",
+ aliases: ["glsl"],
+ },
+ graphql: {
+ name: "GraphQL",
+ aliases: ["graphql", "gql"],
+ },
+ haml: {
+ name: "Ruby Haml",
+ aliases: ["haml"],
+ },
+ html: {
+ name: "HTML",
+ aliases: ["html"],
+ },
+ java: {
+ name: "Java",
+ aliases: ["java"],
+ },
+ javascript: {
+ name: "JavaScript",
+ aliases: ["javascript", "js"],
+ },
+ json: {
+ name: "JSON",
+ aliases: ["json"],
+ },
+ jsonc: {
+ name: "JSON with Comments",
+ aliases: ["jsonc"],
+ },
+ jsonl: {
+ name: "JSON Lines",
+ aliases: ["jsonl"],
+ },
+ jsx: {
+ name: "JSX",
+ aliases: ["jsx"],
+ },
+ julia: {
+ name: "Julia",
+ aliases: ["julia", "jl"],
+ },
+ less: {
+ name: "Less",
+ aliases: ["less"],
+ },
+ markdown: {
+ name: "Markdown",
+ aliases: ["markdown", "md"],
+ },
+ mdx: {
+ name: "MDX",
+ aliases: ["mdx"],
+ },
+ php: {
+ name: "PHP",
+ aliases: ["php"],
+ },
+ postcss: {
+ name: "PostCSS",
+ aliases: ["postcss"],
+ },
+ pug: {
+ name: "Pug",
+ aliases: ["pug", "jade"],
+ },
+ python: {
+ name: "Python",
+ aliases: ["python", "py"],
+ },
+ r: {
+ name: "R",
+ aliases: ["r"],
+ },
+ regexp: {
+ name: "RegExp",
+ aliases: ["regexp", "regex"],
+ },
+ sass: {
+ name: "Sass",
+ aliases: ["sass"],
+ },
+ scss: {
+ name: "SCSS",
+ aliases: ["scss"],
+ },
+ shellscript: {
+ name: "Shell",
+ aliases: ["shellscript", "bash", "sh", "shell", "zsh"],
+ },
+ sql: {
+ name: "SQL",
+ aliases: ["sql"],
+ },
+ svelte: {
+ name: "Svelte",
+ aliases: ["svelte"],
+ },
+ typescript: {
+ name: "TypeScript",
+ aliases: ["typescript", "ts"],
+ },
+ vue: {
+ name: "Vue",
+ aliases: ["vue"],
+ },
+ "vue-html": {
+ name: "Vue HTML",
+ aliases: ["vue-html"],
+ },
+ wasm: {
+ name: "WebAssembly",
+ aliases: ["wasm"],
+ },
+ wgsl: {
+ name: "WGSL",
+ aliases: ["wgsl"],
+ },
+ xml: {
+ name: "XML",
+ aliases: ["xml"],
+ },
+ yaml: {
+ name: "YAML",
+ aliases: ["yaml", "yml"],
+ },
+ tsx: {
+ name: "TSX",
+ aliases: ["tsx", "typescriptreact"],
+ },
+ haskell: {
+ name: "Haskell",
+ aliases: ["haskell", "hs"],
+ },
+ csharp: {
+ name: "C#",
+ aliases: ["c#", "csharp", "cs"],
+ },
+ latex: {
+ name: "LaTeX",
+ aliases: ["latex"],
+ },
+ lua: {
+ name: "Lua",
+ aliases: ["lua"],
+ },
+ mermaid: {
+ name: "Mermaid",
+ aliases: ["mermaid", "mmd"],
+ },
+ ruby: {
+ name: "Ruby",
+ aliases: ["ruby", "rb"],
+ },
+ rust: {
+ name: "Rust",
+ aliases: ["rust", "rs"],
+ },
+ scala: {
+ name: "Scala",
+ aliases: ["scala"],
+ },
+ swift: {
+ name: "Swift",
+ aliases: ["swift"],
+ },
+ kotlin: {
+ name: "Kotlin",
+ aliases: ["kotlin", "kt", "kts"],
+ },
+ "objective-c": {
+ name: "Objective C",
+ aliases: ["objective-c", "objc"],
+ },
+ },
+ createHighlighter: () =>
+ createHighlighter({
+ themes: ["github-dark", "github-light"],
+ langs: [],
+ }),
+} satisfies CodeBlockOptions;
diff --git a/packages/code-block/src/shiki.bundle.ts b/packages/code-block/src/shiki.bundle.ts
new file mode 100644
index 0000000000..41363e1f00
--- /dev/null
+++ b/packages/code-block/src/shiki.bundle.ts
@@ -0,0 +1,103 @@
+/* Generate by @shikijs/codegen */
+import type {
+ DynamicImportLanguageRegistration,
+ DynamicImportThemeRegistration,
+ HighlighterGeneric,
+} from "@shikijs/types";
+import { createBundledHighlighter } from "@shikijs/core";
+import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";
+
+type BundledLanguage = "typescript" | "ts" | "javascript" | "js" | "vue";
+type BundledTheme = "github-light" | "github-dark";
+type Highlighter = HighlighterGeneric;
+
+const bundledLanguages = {
+ c: () => import("@shikijs/langs-precompiled/c"),
+ cpp: () => import("@shikijs/langs-precompiled/cpp"),
+ "c++": () => import("@shikijs/langs-precompiled/cpp"),
+ css: () => import("@shikijs/langs-precompiled/css"),
+ glsl: () => import("@shikijs/langs-precompiled/glsl"),
+ graphql: () => import("@shikijs/langs-precompiled/graphql"),
+ gql: () => import("@shikijs/langs-precompiled/graphql"),
+ haml: () => import("@shikijs/langs-precompiled/haml"),
+ html: () => import("@shikijs/langs-precompiled/html"),
+ java: () => import("@shikijs/langs-precompiled/java"),
+ javascript: () => import("@shikijs/langs-precompiled/javascript"),
+ js: () => import("@shikijs/langs-precompiled/javascript"),
+ json: () => import("@shikijs/langs-precompiled/json"),
+ jsonc: () => import("@shikijs/langs-precompiled/jsonc"),
+ jsonl: () => import("@shikijs/langs-precompiled/jsonl"),
+ jsx: () => import("@shikijs/langs-precompiled/jsx"),
+ julia: () => import("@shikijs/langs-precompiled/julia"),
+ jl: () => import("@shikijs/langs-precompiled/julia"),
+ less: () => import("@shikijs/langs-precompiled/less"),
+ markdown: () => import("@shikijs/langs-precompiled/markdown"),
+ md: () => import("@shikijs/langs-precompiled/markdown"),
+ mdx: () => import("@shikijs/langs-precompiled/mdx"),
+ php: () => import("@shikijs/langs-precompiled/php"),
+ postcss: () => import("@shikijs/langs-precompiled/postcss"),
+ pug: () => import("@shikijs/langs-precompiled/pug"),
+ jade: () => import("@shikijs/langs-precompiled/pug"),
+ python: () => import("@shikijs/langs-precompiled/python"),
+ py: () => import("@shikijs/langs-precompiled/python"),
+ r: () => import("@shikijs/langs-precompiled/r"),
+ regexp: () => import("@shikijs/langs-precompiled/regexp"),
+ regex: () => import("@shikijs/langs-precompiled/regexp"),
+ sass: () => import("@shikijs/langs-precompiled/sass"),
+ scss: () => import("@shikijs/langs-precompiled/scss"),
+ shellscript: () => import("@shikijs/langs-precompiled/shellscript"),
+ bash: () => import("@shikijs/langs-precompiled/shellscript"),
+ sh: () => import("@shikijs/langs-precompiled/shellscript"),
+ shell: () => import("@shikijs/langs-precompiled/shellscript"),
+ zsh: () => import("@shikijs/langs-precompiled/shellscript"),
+ sql: () => import("@shikijs/langs-precompiled/sql"),
+ svelte: () => import("@shikijs/langs-precompiled/svelte"),
+ typescript: () => import("@shikijs/langs-precompiled/typescript"),
+ ts: () => import("@shikijs/langs-precompiled/typescript"),
+ vue: () => import("@shikijs/langs-precompiled/vue"),
+ "vue-html": () => import("@shikijs/langs-precompiled/vue-html"),
+ wasm: () => import("@shikijs/langs-precompiled/wasm"),
+ wgsl: () => import("@shikijs/langs-precompiled/wgsl"),
+ xml: () => import("@shikijs/langs-precompiled/xml"),
+ yaml: () => import("@shikijs/langs-precompiled/yaml"),
+ yml: () => import("@shikijs/langs-precompiled/yaml"),
+ tsx: () => import("@shikijs/langs-precompiled/tsx"),
+ typescriptreact: () => import("@shikijs/langs-precompiled/tsx"),
+ haskell: () => import("@shikijs/langs-precompiled/haskell"),
+ hs: () => import("@shikijs/langs-precompiled/haskell"),
+ "c#": () => import("@shikijs/langs-precompiled/csharp"),
+ csharp: () => import("@shikijs/langs-precompiled/csharp"),
+ cs: () => import("@shikijs/langs-precompiled/csharp"),
+ latex: () => import("@shikijs/langs-precompiled/latex"),
+ lua: () => import("@shikijs/langs-precompiled/lua"),
+ mermaid: () => import("@shikijs/langs-precompiled/mermaid"),
+ mmd: () => import("@shikijs/langs-precompiled/mermaid"),
+ ruby: () => import("@shikijs/langs-precompiled/ruby"),
+ rb: () => import("@shikijs/langs-precompiled/ruby"),
+ rust: () => import("@shikijs/langs-precompiled/rust"),
+ rs: () => import("@shikijs/langs-precompiled/rust"),
+ scala: () => import("@shikijs/langs-precompiled/scala"),
+ swift: () => import("@shikijs/langs-precompiled/swift"),
+ kotlin: () => import("@shikijs/langs-precompiled/kotlin"),
+ kt: () => import("@shikijs/langs-precompiled/kotlin"),
+ kts: () => import("@shikijs/langs-precompiled/kotlin"),
+ "objective-c": () => import("@shikijs/langs-precompiled/objective-c"),
+ objc: () => import("@shikijs/langs-precompiled/objective-c"),
+} as Record;
+
+const bundledThemes = {
+ "github-dark": () => import("@shikijs/themes/github-dark"),
+ "github-light": () => import("@shikijs/themes/github-light"),
+} as Record;
+
+const createHighlighter = /* @__PURE__ */ createBundledHighlighter<
+ BundledLanguage,
+ BundledTheme
+>({
+ langs: bundledLanguages,
+ themes: bundledThemes,
+ engine: () => createJavaScriptRegexEngine(),
+});
+
+export { createHighlighter };
+export type { BundledLanguage, BundledTheme, Highlighter };
diff --git a/packages/code-block/tsconfig.json b/packages/code-block/tsconfig.json
new file mode 100644
index 0000000000..30ab55f7ca
--- /dev/null
+++ b/packages/code-block/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "outDir": "dist",
+ "declaration": true,
+ "declarationDir": "types",
+ "composite": true,
+ "skipLibCheck": true,
+ "emitDeclarationOnly": true,
+ "paths": {
+ "@shikijs/types": ["../../node_modules/@shikijs/types"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/packages/code-block/vite.config.ts b/packages/code-block/vite.config.ts
new file mode 100644
index 0000000000..cb9f20516e
--- /dev/null
+++ b/packages/code-block/vite.config.ts
@@ -0,0 +1,65 @@
+import * as path from "path";
+import { webpackStats } from "rollup-plugin-webpack-stats";
+import { defineConfig } from "vite";
+import pkg from "./package.json";
+// import eslintPlugin from "vite-plugin-eslint";
+
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ test: {
+ setupFiles: ["./vitestSetup.ts"],
+ },
+ plugins: [webpackStats() as any],
+ // used so that vitest resolves the core package from the sources instead of the built version
+ resolve: {
+ alias:
+ conf.command === "build"
+ ? ({} as Record)
+ : ({
+ // load live from sources with live reload working
+ "@blocknote/core": path.resolve(__dirname, "../core/src/"),
+ "@blocknote/react": path.resolve(__dirname, "../react/src/"),
+ } as Record),
+ },
+ build: {
+ sourcemap: true,
+ lib: {
+ entry: {
+ "blocknote-code-block": path.resolve(__dirname, "src/index.ts"),
+ },
+ name: "blocknote-code-block",
+ formats: ["es", "cjs"],
+ fileName: (format, entryName) =>
+ format === "es" ? `${entryName}.js` : `${entryName}.cjs`,
+ },
+ rollupOptions: {
+ // make sure to externalize deps that shouldn't be bundled
+ // into your library
+ external: (source) => {
+ if (
+ Object.keys({
+ ...pkg.dependencies,
+ ...((pkg as any).peerDependencies || {}),
+ ...pkg.devDependencies,
+ }).some((dep) => source === dep || source.startsWith(dep + "/"))
+ ) {
+ return true;
+ }
+ return (
+ source.startsWith("react/") ||
+ source.startsWith("react-dom/") ||
+ source.startsWith("prosemirror-") ||
+ source.startsWith("@tiptap/") ||
+ source.startsWith("@blocknote/") ||
+ source.startsWith("@shikijs/") ||
+ source.startsWith("node:")
+ );
+ },
+ output: {
+ // Provide global variables to use in the UMD build
+ // for externalized deps
+ globals: {},
+ },
+ },
+ },
+}));
diff --git a/packages/code-block/vitestSetup.ts b/packages/code-block/vitestSetup.ts
new file mode 100644
index 0000000000..a946b5fc3a
--- /dev/null
+++ b/packages/code-block/vitestSetup.ts
@@ -0,0 +1,10 @@
+import { afterEach, beforeEach } from "vitest";
+
+beforeEach(() => {
+ globalThis.window = globalThis.window || ({} as any);
+ (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {};
+});
+
+afterEach(() => {
+ delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS;
+});
diff --git a/packages/core/.gitignore b/packages/core/.gitignore
index a547bf36d8..58f115c8dc 100644
--- a/packages/core/.gitignore
+++ b/packages/core/.gitignore
@@ -5,7 +5,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
-lerna-debug.log*
node_modules
dist
diff --git a/packages/core/ARCHITECTURE.md b/packages/core/ARCHITECTURE.md
deleted file mode 100644
index ee07f25ea3..0000000000
--- a/packages/core/ARCHITECTURE.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Node structure
-
-We use a Prosemirror document structure where every element is a `block` with 1 `content` element and one optional group of children (`blockgroup`).
-
-- A `block` can only appear in a `blockgroup` (which is also the type of the root node)
-- Every `block` element can have attributes (e.g.: is it a heading or a list item)
-- Every `block` element can contain a `blockgroup` as second child. In this case the `blockgroup` is considered nested (indented in the UX)
-
-This architecture is different from the "default" Prosemirror / Tiptap implementation which would use more semantic HTML node types (`p`, `li`, etc.). We have designed this block structure instead to more easily:
-
-- support indentation of any node (without complex wrapping logic)
-- supporting animations (nodes stay the same type, only attrs are changed)
-
-## Example
-
-```xml
-
-
- Parent element 1
-
-
- Nested / child / indented item
-
-
-
-
- Parent element 2
-
- ...
- ...
-
-
-
- Element 3 without children
-
-
-```
diff --git a/packages/core/LICENSE b/packages/core/LICENSE
new file mode 100644
index 0000000000..fa0086a952
--- /dev/null
+++ b/packages/core/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
\ No newline at end of file
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 0000000000..f391efb3f3
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,107 @@
+
) move it forward so we include all closing tags (|)
+ while (end.parentOffset >= end.parent.nodeSize - 2 && end.depth > 0) {
+ end = tr.doc.resolve(end.pos + 1);
+ }
+
+ // if the end is at the start of an empty node (
|) move it backwards so we drop empty start tags (
|)
+ while (end.parentOffset === 0 && end.depth > 0) {
+ end = tr.doc.resolve(end.pos - 1);
+ }
+
+ // if the start is at the start of a node (
|) move it backwards so we include all open tags (|
)
+ while (start.parentOffset === 0 && start.depth > 0) {
+ start = tr.doc.resolve(start.pos - 1);
+ }
+
+ // if the start is at the end of a node (|
|) move it forwards so we drop all closing tags (|
+ *
+ * We have a problem though, from the block json, there is no way to tell that the cell "2-1" is the second cell in the second row.
+ * To resolve this, we created the occupancy grid, which is a grid of all the cells in the table, as though they were only 1x1 cells.
+ * See {@link OccupancyGrid} for more information.
+ *
+ */
+
+/**
+ * Relative cell indices are relative to the table block's content.
+ *
+ * This is a sparse representation of the table and is how HTML and BlockNote JSON represent tables.
+ *
+ * For example, if we have a table with a rowspan of 2, the second row may only have 1 element in a 2x2 table.
+ *
+ * ```
+ * // Visual representation of the table
+ * | 1-1 | 1-2 | // has 2 cells
+ * | 1-1 | 2-2 | // has only 1 cell
+ * // Relative cell indices
+ * [{ row: 1, col: 1, rowspan: 2 }, { row: 1, col: 2 }] // has 2 cells
+ * [{ row: 1, col: 2 }] // has only 1 cell
+ * ```
+ */
+export type RelativeCellIndices = {
+ row: number;
+ col: number;
+};
+
+/**
+ * Absolute cell indices are relative to the table's layout (it's {@link OccupancyGrid}).
+ *
+ * It is as though the table is a grid of 1x1 cells, and any colspan or rowspan results in multiple 1x1 cells being occupied.
+ *
+ * For example, if we have a table with a colspan of 2, it will occupy 2 cells in the layout grid.
+ *
+ * ```
+ * // Visual representation of the table
+ * | 1-1 | 1-1 | // has 2 cells
+ * | 2-1 | 2-2 | // has 2 cell
+ * // Absolute cell indices
+ * [{ row: 1, col: 1, colspan: 2 }, { row: 1, col: 2, colspan: 2 }] // has 2 cells
+ * [{ row: 1, col: 1 }, { row: 1, col: 2 }] // has 2 cells
+ * ```
+ */
+export type AbsoluteCellIndices = {
+ row: number;
+ col: number;
+};
+
+/**
+ * An occupancy grid is a grid of the occupied cells in the table.
+ * It is used to track the occupied cells in the table to know where to place the next cell.
+ *
+ * Since it allows us to resolve cell indices both {@link RelativeCellIndices} and {@link AbsoluteCellIndices}, it is the core data structure for table operations.
+ */
+type OccupancyGrid = (RelativeCellIndices & {
+ /**
+ * The rowspan of the cell.
+ */
+ rowspan: number;
+ /**
+ * The colspan of the cell.
+ */
+ colspan: number;
+ /**
+ * The cell.
+ */
+ cell: TableCell;
+})[][];
+
+/**
+ * This will return the {@link OccupancyGrid} of the table.
+ * By laying out the table as though it were a grid of 1x1 cells, we can easily track where the cells are located (both relatively and absolutely).
+ *
+ * @returns an {@link OccupancyGrid}
+ */
+export function getTableCellOccupancyGrid(
+ block: BlockFromConfigNoChildren,
+): OccupancyGrid {
+ const { height, width } = getDimensionsOfTable(block);
+
+ /**
+ * Create a grid to track occupied cells
+ * This is used because rowspans and colspans take up multiple spaces
+ * So, we need to track the occupied cells in the grid to know where to place the next cell
+ */
+ const grid: OccupancyGrid = new Array(height)
+ .fill(false)
+ .map(() => new Array(width).fill(null));
+
+ // Find the next unoccupied cell in the table, row-major order
+ const findNextAvailable = (row: number, col: number) => {
+ for (let i = row; i < height; i++) {
+ for (let j = col; j < width; j++) {
+ if (!grid[i][j]) {
+ return { row: i, col: j };
+ }
+ }
+ }
+
+ throw new Error(
+ "Unable to create occupancy grid for table, no more available cells",
+ );
+ };
+
+ // Build up the grid, trying to fill in the cells with the correct relative row and column indices
+ for (let row = 0; row < block.content.rows.length; row++) {
+ for (let col = 0; col < block.content.rows[row].cells.length; col++) {
+ const cell = mapTableCell(block.content.rows[row].cells[col]);
+ const rowspan = getRowspan(cell);
+ const colspan = getColspan(cell);
+
+ // Rowspan and colspan complicate things, by taking up multiple cells in the grid
+ // We need to iterate over the cells that the rowspan and colspan take up
+ // and fill in the grid with the correct relative row and column indices
+ const { row: startRow, col: startCol } = findNextAvailable(row, col);
+
+ // Fill in the rowspan X colspan cells, starting from the next available cell, with the correct relative row and column indices
+ for (let i = startRow; i < startRow + rowspan; i++) {
+ for (let j = startCol; j < startCol + colspan; j++) {
+ if (grid[i][j]) {
+ // The cell is already occupied, the table is malformed
+ throw new Error(
+ `Unable to create occupancy grid for table, cell at ${i},${j} is already occupied`,
+ );
+ }
+
+ grid[i][j] = {
+ row,
+ col,
+ rowspan,
+ colspan,
+ cell,
+ };
+ }
+ }
+ }
+ }
+
+ // console.log(grid);
+
+ return grid;
+}
+
+/**
+ * Given an {@link OccupancyGrid}, this will return the {@link TableContent} rows.
+ *
+ * @note This will remove duplicates from the occupancy grid. And does no bounds checking for validity of the occupancy grid.
+ */
+export function getTableRowsFromOccupancyGrid(
+ occupancyGrid: OccupancyGrid,
+): TableContent["rows"] {
+ // Because a cell can have a rowspan or colspan, it can occupy multiple cells in the occupancy grid
+ // So, we need to remove duplicates from the occupancy grid before we can return the table rows
+ const seen = new Set();
+
+ return occupancyGrid.map((row) => {
+ // Just read out the cells in the occupancy grid, removing duplicates
+ return {
+ cells: row
+ .map((cell) => {
+ if (seen.has(cell.row + ":" + cell.col)) {
+ return false;
+ }
+ seen.add(cell.row + ":" + cell.col);
+ return cell.cell;
+ })
+ .filter((cell): cell is TableCell => cell !== false),
+ };
+ });
+}
+
+/**
+ * This will resolve the relative cell indices within the table block to the absolute cell indices within the table, accounting for colspan and rowspan.
+ *
+ * @note It will return only the first cell (i.e. top-left) that matches the relative cell indices. To find the other absolute cell indices this cell occupies, you can assume it is the rowspan and colspan number of cells away from the top-left cell.
+ *
+ * @returns The {@link AbsoluteCellIndices} and the {@link TableCell} at the absolute position.
+ */
+export function getAbsoluteTableCells(
+ /**
+ * The relative position of the cell in the table.
+ */
+ relativeCellIndices: RelativeCellIndices,
+ /**
+ * The table block containing the cell.
+ */
+ block: BlockFromConfigNoChildren,
+ /**
+ * The occupancy grid of the table.
+ */
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+): AbsoluteCellIndices & {
+ cell: TableCell;
+} {
+ for (let r = 0; r < occupancyGrid.length; r++) {
+ for (let c = 0; c < occupancyGrid[r].length; c++) {
+ const cell = occupancyGrid[r][c];
+ if (
+ cell &&
+ cell.row === relativeCellIndices.row &&
+ cell.col === relativeCellIndices.col
+ ) {
+ return { row: r, col: c, cell: cell.cell };
+ }
+ }
+ }
+
+ throw new Error(
+ `Unable to resolve relative table cell indices for table, cell at ${relativeCellIndices.row},${relativeCellIndices.col} is not occupied`,
+ );
+}
+
+/**
+ * This will get the dimensions of the table block.
+ *
+ * @returns The height and width of the table.
+ */
+export function getDimensionsOfTable(
+ block: BlockFromConfigNoChildren,
+): {
+ /**
+ * The number of rows in the table.
+ */
+ height: number;
+ /**
+ * The number of columns in the table.
+ */
+ width: number;
+} {
+ // Due to the way we store the table, the height is always the number of rows
+ const height = block.content.rows.length;
+
+ // Calculating the width is a bit more complex, as it is the maximum width of any row
+ let width = 0;
+ block.content.rows.forEach((row) => {
+ // Find the width of the row by summing the colspan of each cell
+ let rowWidth = 0;
+ row.cells.forEach((cell) => {
+ rowWidth += getColspan(cell);
+ });
+
+ // Update the width if the row is wider than the current width
+ width = Math.max(width, rowWidth);
+ });
+
+ return { height, width };
+}
+
+/**
+ * This will resolve the absolute cell indices within the table block to the relative cell indices within the table, accounting for colspan and rowspan.
+ *
+ * @returns The {@link RelativeCellIndices} and the {@link TableCell} at the relative position.
+ */
+export function getRelativeTableCells(
+ /**
+ * The {@link AbsoluteCellIndices} of the cell in the table.
+ */
+ absoluteCellIndices: AbsoluteCellIndices,
+ /**
+ * The table block containing the cell.
+ */
+ block: BlockFromConfigNoChildren,
+ /**
+ * The occupancy grid of the table.
+ */
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+):
+ | (RelativeCellIndices & {
+ cell: TableContent["rows"][number]["cells"][number];
+ })
+ | undefined {
+ const occupancyCell =
+ occupancyGrid[absoluteCellIndices.row]?.[absoluteCellIndices.col];
+
+ // Double check that the cell can be accessed
+ if (!occupancyCell) {
+ // The cell is not occupied, so it is invalid
+ return undefined;
+ }
+
+ return {
+ row: occupancyCell.row,
+ col: occupancyCell.col,
+ cell: occupancyCell.cell,
+ };
+}
+
+/**
+ * This will get all the cells within a relative row of a table block.
+ *
+ * This method always starts the search for the row at the first column of the table.
+ *
+ * ```
+ * // Visual representation of a table
+ * | A | B | C |
+ * | | D | E |
+ * | F | G | H |
+ * // "A" has a rowspan of 2
+ *
+ * // getCellsAtRowHandle(0)
+ * // returns [
+ * { row: 0, col: 0, cell: "A" },
+ * { row: 0, col: 1, cell: "B" },
+ * { row: 0, col: 2, cell: "C" },
+ * ]
+ *
+ * // getCellsAtColumnHandle(1)
+ * // returns [
+ * { row: 1, col: 0, cell: "F" },
+ * { row: 1, col: 1, cell: "G" },
+ * { row: 1, col: 2, cell: "H" },
+ * ]
+ * ```
+ *
+ * As you can see, you may not be able to retrieve all nodes given a relative row index, as cells can span multiple rows.
+ *
+ * @returns All of the cells associated with the relative row of the table. (All cells that have the same relative row index)
+ */
+export function getCellsAtRowHandle(
+ block: BlockFromConfigNoChildren,
+ relativeRowIndex: RelativeCellIndices["row"],
+) {
+ const occupancyGrid = getTableCellOccupancyGrid(block);
+
+ if (relativeRowIndex < 0 || relativeRowIndex >= occupancyGrid.length) {
+ return [];
+ }
+
+ // First need to resolve the relative row index to an absolute row index
+ let absoluteRow = 0;
+
+ // Jump through the occupied cells ${relativeCellIndices.row} times to find the absolute row position
+ for (let i = 0; i < relativeRowIndex; i++) {
+ const cell = occupancyGrid[absoluteRow]?.[0];
+
+ if (!cell) {
+ return [];
+ }
+
+ // Skip the cells that the rowspan takes up
+ absoluteRow += cell.rowspan;
+ }
+
+ // Then for each column, get the cell at the absolute row index as a relative cell index
+ const cells = new Array(occupancyGrid[0].length)
+ .fill(false)
+ .map((_v, col) => {
+ return getRelativeTableCells(
+ { row: absoluteRow, col },
+ block,
+ occupancyGrid,
+ );
+ })
+ .filter(
+ (a): a is RelativeCellIndices & { cell: TableCell } =>
+ a !== undefined,
+ );
+
+ // Filter out duplicates based on row and col properties
+ return cells.filter((cell, index) => {
+ return (
+ cells.findIndex((c) => c.row === cell.row && c.col === cell.col) === index
+ );
+ });
+}
+
+/**
+ * This will get all the cells within a relative column of a table block.
+ *
+ * This method always starts the search for the column at the first row of the table.
+ *
+ * ```
+ * // Visual representation of a table
+ * | A | B |
+ * | C | D | E |
+ * | F | G | H |
+ * // "A" has a colspan of 2
+ *
+ * // getCellsAtColumnHandle(0)
+ * // returns [
+ * { row: 0, col: 0, cell: "A" },
+ * { row: 1, col: 0, cell: "C" },
+ * { row: 2, col: 0, cell: "F" },
+ * ]
+ *
+ * // getCellsAtColumnHandle(1)
+ * // returns [
+ * { row: 0, col: 1, cell: "B" },
+ * { row: 1, col: 2, cell: "E" },
+ * { row: 2, col: 2, cell: "F" },
+ * ]
+ * ```
+ *
+ * As you can see, you may not be able to retrieve all nodes given a relative column index, as cells can span multiple columns.
+ *
+ * @returns All of the cells associated with the relative column of the table. (All cells that have the same relative column index)
+ */
+export function getCellsAtColumnHandle(
+ block: BlockFromConfigNoChildren,
+ relativeColumnIndex: RelativeCellIndices["col"],
+) {
+ const occupancyGrid = getTableCellOccupancyGrid(block);
+
+ if (
+ relativeColumnIndex < 0 ||
+ relativeColumnIndex >= occupancyGrid[0].length
+ ) {
+ return [];
+ }
+
+ // First need to resolve the relative column index to an absolute column index
+ let absoluteCol = 0;
+
+ // Now that we've already resolved the absolute row position, we can jump through the occupied cells ${relativeCellIndices.col} times to find the absolute column position
+ for (let i = 0; i < relativeColumnIndex; i++) {
+ const cell = occupancyGrid[0]?.[absoluteCol];
+
+ if (!cell) {
+ return [];
+ }
+
+ // Skip the cells that the colspan takes up
+ absoluteCol += cell.colspan;
+ }
+
+ // Then for each row, get the cell at the absolute column index as a relative cell index
+ const cells = new Array(occupancyGrid.length)
+ .fill(false)
+ .map((_v, row) => {
+ return getRelativeTableCells(
+ { row, col: absoluteCol },
+ block,
+ occupancyGrid,
+ );
+ })
+ .filter(
+ (a): a is RelativeCellIndices & { cell: TableCell } =>
+ a !== undefined,
+ );
+
+ // Filter out duplicates based on row and col properties
+ return cells.filter((cell, index) => {
+ return (
+ cells.findIndex((c) => c.row === cell.row && c.col === cell.col) === index
+ );
+ });
+}
+
+/**
+ * This moves a column from one index to another.
+ *
+ * @note This is a destructive operation, it will modify the provided {@link OccupancyGrid} in place.
+ */
+export function moveColumn(
+ block: BlockFromConfigNoChildren,
+ fromColIndex: RelativeCellIndices["col"],
+ toColIndex: RelativeCellIndices["col"],
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+): TableContent["rows"] {
+ // To move cells in a column, we need to layout the whole table
+ // and then move the cells accordingly.
+ const { col: absoluteSourceCol } = getAbsoluteTableCells(
+ {
+ row: 0,
+ col: fromColIndex,
+ },
+ block,
+ occupancyGrid,
+ );
+ const { col: absoluteTargetCol } = getAbsoluteTableCells(
+ {
+ row: 0,
+ col: toColIndex,
+ },
+ block,
+ occupancyGrid,
+ );
+
+ /**
+ * Currently, this function assumes that the caller has already checked that the source and target columns are valid.
+ * Such as by using {@link canColumnBeDraggedInto}. In the future, we may want to have the move logic be smarter
+ * and handle invalid column indices in some way.
+ */
+ occupancyGrid.forEach((row) => {
+ // Move the cell to the target column
+ const [sourceCell] = row.splice(absoluteSourceCol, 1);
+ row.splice(absoluteTargetCol, 0, sourceCell);
+ });
+
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
+}
+
+/**
+ * This moves a row from one index to another.
+ *
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
+ */
+export function moveRow(
+ block: BlockFromConfigNoChildren,
+ fromRowIndex: RelativeCellIndices["row"],
+ toRowIndex: RelativeCellIndices["row"],
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+): TableContent["rows"] {
+ // To move cells in a column, we need to layout the whole table
+ // and then move the cells accordingly.
+ const { row: absoluteSourceRow } = getAbsoluteTableCells(
+ {
+ row: fromRowIndex,
+ col: 0,
+ },
+ block,
+ occupancyGrid,
+ );
+ const { row: absoluteTargetRow } = getAbsoluteTableCells(
+ {
+ row: toRowIndex,
+ col: 0,
+ },
+ block,
+ occupancyGrid,
+ );
+
+ /**
+ * Currently, this function assumes that the caller has already checked that the source and target rows are valid.
+ * Such as by using {@link canRowBeDraggedInto}. In the future, we may want to have the move logic be smarter
+ * and handle invalid row indices in some way.
+ */
+ const [sourceRow] = occupancyGrid.splice(absoluteSourceRow, 1);
+ occupancyGrid.splice(absoluteTargetRow, 0, sourceRow);
+
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
+}
+
+/**
+ * This will check if a cell is empty.
+ *
+ * @returns True if the cell is empty, false otherwise.
+ */
+function isCellEmpty(
+ cell:
+ | PartialTableContent["rows"][number]["cells"][number]
+ | undefined,
+): boolean {
+ if (!cell) {
+ return true;
+ }
+ if (isPartialTableCell(cell)) {
+ return isCellEmpty(cell.content);
+ } else if (typeof cell === "string") {
+ return cell.length === 0;
+ } else if (Array.isArray(cell)) {
+ return cell.every((c) =>
+ typeof c === "string"
+ ? c.length === 0
+ : isStyledTextInlineContent(c)
+ ? c.text.length === 0
+ : isPartialLinkInlineContent(c)
+ ? typeof c.content === "string"
+ ? c.content.length === 0
+ : c.content.every((s) => s.text.length === 0)
+ : false,
+ );
+ } else {
+ return false;
+ }
+}
+
+/**
+ * This will remove empty rows or columns from the table.
+ *
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
+ */
+export function cropEmptyRowsOrColumns(
+ block: BlockFromConfigNoChildren,
+ removeEmpty: "columns" | "rows",
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+): TableContent["rows"] {
+ if (removeEmpty === "columns") {
+ // strips empty columns on the right
+ let emptyColsOnRight = 0;
+ for (
+ let cellIndex = occupancyGrid[0].length - 1;
+ cellIndex >= 0;
+ cellIndex--
+ ) {
+ const isEmpty = occupancyGrid.every(
+ (row) =>
+ isCellEmpty(row[cellIndex].cell) && row[cellIndex].colspan === 1,
+ );
+ if (!isEmpty) {
+ break;
+ }
+
+ emptyColsOnRight++;
+ }
+
+ for (let i = occupancyGrid.length - 1; i >= 0; i--) {
+ // We maintain at least one cell, even if all the cells are empty
+ const cellsToRemove = Math.max(
+ occupancyGrid[i].length - emptyColsOnRight,
+ 1,
+ );
+ occupancyGrid[i] = occupancyGrid[i].slice(0, cellsToRemove);
+ }
+
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
+ }
+
+ // strips empty rows at the bottom
+ let emptyRowsOnBottom = 0;
+ for (let rowIndex = occupancyGrid.length - 1; rowIndex >= 0; rowIndex--) {
+ const isEmpty = occupancyGrid[rowIndex].every(
+ (cell) => isCellEmpty(cell.cell) && cell.rowspan === 1,
+ );
+ if (!isEmpty) {
+ break;
+ }
+
+ emptyRowsOnBottom++;
+ }
+
+ // We maintain at least one row, even if all the rows are empty
+ const rowsToRemove = Math.min(emptyRowsOnBottom, occupancyGrid.length - 1);
+
+ occupancyGrid.splice(occupancyGrid.length - rowsToRemove, rowsToRemove);
+
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
+}
+
+/**
+ * This will add a specified number of rows or columns to the table (filling with empty cells).
+ *
+ * @note This is a destructive operation, it will modify the {@link OccupancyGrid} in place.
+ */
+export function addRowsOrColumns(
+ block: BlockFromConfigNoChildren,
+ addType: "columns" | "rows",
+ /**
+ * The number of rows or columns to add.
+ *
+ * @note if negative, it will remove rows or columns.
+ */
+ numToAdd: number,
+ occupancyGrid: OccupancyGrid = getTableCellOccupancyGrid(block),
+): TableContent["rows"] {
+ const { width, height } = getDimensionsOfTable(block);
+
+ if (addType === "columns") {
+ // Add empty columns to the right
+ occupancyGrid.forEach((row, rowIndex) => {
+ if (numToAdd >= 0) {
+ for (let i = 0; i < numToAdd; i++) {
+ row.push({
+ row: rowIndex,
+ col: Math.max(...row.map((r) => r.col)) + 1,
+ rowspan: 1,
+ colspan: 1,
+ cell: mapTableCell(""),
+ });
+ }
+ } else {
+ // Remove columns on the right
+ row.splice(width + numToAdd, -1 * numToAdd);
+ }
+ });
+ } else {
+ if (numToAdd > 0) {
+ // Add empty rows to the bottom
+ for (let i = 0; i < numToAdd; i++) {
+ const newRow = new Array(width).fill(null).map((_, colIndex) => ({
+ row: height + i,
+ col: colIndex,
+ rowspan: 1,
+ colspan: 1,
+ cell: mapTableCell(""),
+ }));
+ occupancyGrid.push(newRow);
+ }
+ } else if (numToAdd < 0) {
+ // Remove rows at the bottom
+ occupancyGrid.splice(height + numToAdd, -1 * numToAdd);
+ }
+ }
+
+ return getTableRowsFromOccupancyGrid(occupancyGrid);
+}
+
+/**
+ * Checks if a row can be safely dropped at the target row index without splitting merged cells.
+ */
+export function canRowBeDraggedInto(
+ block: BlockFromConfigNoChildren,
+ draggingIndex: RelativeCellIndices["row"],
+ targetRowIndex: RelativeCellIndices["row"],
+) {
+ // Check cells at the target row
+ const targetCells = getCellsAtRowHandle(block, targetRowIndex);
+
+ // If no cells have rowspans > 1, dragging is always allowed
+ const hasMergedCells = targetCells.some((cell) => getRowspan(cell.cell) > 1);
+ if (!hasMergedCells) {
+ return true;
+ }
+
+ let endRowIndex = targetRowIndex;
+ let startRowIndex = targetRowIndex;
+ targetCells.forEach((cell) => {
+ const rowspan = getRowspan(cell.cell);
+ endRowIndex = Math.max(endRowIndex, cell.row + rowspan - 1);
+ startRowIndex = Math.min(startRowIndex, cell.row);
+ });
+
+ // Check the direction of the drag
+ const isDraggingDown = draggingIndex < targetRowIndex;
+
+ // Allow dragging only at the start/end of merged cells
+ // Otherwise, the target row was within a merged cell which we don't allow
+ return isDraggingDown
+ ? targetRowIndex === endRowIndex
+ : targetRowIndex === startRowIndex;
+}
+
+/**
+ * Checks if a column can be safely dropped at the target column index without splitting merged cells.
+ */
+export function canColumnBeDraggedInto(
+ block: BlockFromConfigNoChildren,
+ draggingIndex: RelativeCellIndices["col"],
+ targetColumnIndex: RelativeCellIndices["col"],
+) {
+ // Check cells at the target column
+ const targetCells = getCellsAtColumnHandle(block, targetColumnIndex);
+
+ // If no cells have colspans > 1, dragging is always allowed
+ const hasMergedCells = targetCells.some((cell) => getColspan(cell.cell) > 1);
+ if (!hasMergedCells) {
+ return true;
+ }
+
+ let endColumnIndex = targetColumnIndex;
+ let startColumnIndex = targetColumnIndex;
+ targetCells.forEach((cell) => {
+ const colspan = getColspan(cell.cell);
+ endColumnIndex = Math.max(endColumnIndex, cell.col + colspan - 1);
+ startColumnIndex = Math.min(startColumnIndex, cell.col);
+ });
+
+ // Check the direction of the drag
+ const isDraggingRight = draggingIndex < targetColumnIndex;
+
+ // Allow dragging only at the start/end of merged cells
+ // Otherwise, the target column was within a merged cell which we don't allow
+ return isDraggingRight
+ ? targetColumnIndex === endColumnIndex
+ : targetColumnIndex === startColumnIndex;
+}
+
+/**
+ * Checks if two cells are in the same column.
+ *
+ * @returns True if the cells are in the same column, false otherwise.
+ */
+export function areInSameColumn(
+ from: RelativeCellIndices,
+ to: RelativeCellIndices,
+ block: BlockFromConfigNoChildren,
+) {
+ // Table indices are relative to the table, so we need to resolve the absolute cell indices
+ const anchorAbsoluteCellIndices = getAbsoluteTableCells(from, block);
+
+ // Table indices are relative to the table, so we need to resolve the absolute cell indices
+ const headAbsoluteCellIndices = getAbsoluteTableCells(to, block);
+
+ // Compare the column indices to determine the merge direction
+ return anchorAbsoluteCellIndices.col === headAbsoluteCellIndices.col;
+}
diff --git a/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
new file mode 100644
index 0000000000..c79400c78a
--- /dev/null
+++ b/packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts
@@ -0,0 +1,8 @@
+export const acceptedMIMETypes = [
+ "vscode-editor-data",
+ "blocknote/html",
+ "text/markdown",
+ "text/html",
+ "text/plain",
+ "Files",
+] as const;
diff --git a/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts
new file mode 100644
index 0000000000..f602ef4a2d
--- /dev/null
+++ b/packages/core/src/api/clipboard/fromClipboard/fileDropExtension.ts
@@ -0,0 +1,55 @@
+import { Extension } from "@tiptap/core";
+import { Plugin } from "prosemirror-state";
+
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
+import { handleFileInsertion } from "./handleFileInsertion.js";
+
+export const createDropFileExtension = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+) =>
+ Extension.create<{ editor: BlockNoteEditor }, undefined>({
+ name: "dropFile",
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ props: {
+ handleDOMEvents: {
+ drop(_view, event) {
+ if (!editor.isEditable) {
+ return;
+ }
+
+ let format: (typeof acceptedMIMETypes)[number] | null = null;
+ for (const mimeType of acceptedMIMETypes) {
+ if (event.dataTransfer!.types.includes(mimeType)) {
+ format = mimeType;
+ break;
+ }
+ }
+ if (format === null) {
+ return true;
+ }
+
+ if (format === "Files") {
+ handleFileInsertion(event, editor);
+ return true;
+ }
+
+ return false;
+ },
+ },
+ },
+ }),
+ ];
+ },
+ });
diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts
new file mode 100644
index 0000000000..ced8f59b14
--- /dev/null
+++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts
@@ -0,0 +1,196 @@
+import { Block, PartialBlock } from "../../../blocks/defaultBlocks.js";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import { getNearestBlockPos } from "../../getBlockInfoFromPos.js";
+import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
+
+function checkFileExtensionsMatch(
+ fileExtension1: string,
+ fileExtension2: string,
+) {
+ if (!fileExtension1.startsWith(".") || !fileExtension2.startsWith(".")) {
+ throw new Error(`The strings provided are not valid file extensions.`);
+ }
+
+ return fileExtension1 === fileExtension2;
+}
+
+function checkMIMETypesMatch(mimeType1: string, mimeType2: string) {
+ const types1 = mimeType1.split("/");
+ const types2 = mimeType2.split("/");
+
+ if (types1.length !== 2) {
+ throw new Error(`The string ${mimeType1} is not a valid MIME type.`);
+ }
+ if (types2.length !== 2) {
+ throw new Error(`The string ${mimeType2} is not a valid MIME type.`);
+ }
+
+ if (types1[1] === "*" || types2[1] === "*") {
+ return types1[0] === types2[0];
+ }
+ if (types1[0] === "*" || types2[0] === "*") {
+ return types1[1] === types2[1];
+ }
+
+ return types1[0] === types2[0] && types1[1] === types2[1];
+}
+
+function insertOrUpdateBlock<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ referenceBlock: Block,
+ newBlock: PartialBlock,
+ placement: "before" | "after" = "after",
+) {
+ let insertedBlockId: string | undefined;
+
+ if (
+ Array.isArray(referenceBlock.content) &&
+ referenceBlock.content.length === 0
+ ) {
+ insertedBlockId = editor.updateBlock(referenceBlock, newBlock).id;
+ } else {
+ insertedBlockId = editor.insertBlocks(
+ [newBlock],
+ referenceBlock,
+ placement,
+ )[0].id;
+ }
+
+ return insertedBlockId;
+}
+
+export async function handleFileInsertion<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(event: DragEvent | ClipboardEvent, editor: BlockNoteEditor) {
+ if (!editor.uploadFile) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ "Attempted ot insert file, but uploadFile is not set in the BlockNote editor options",
+ );
+ return;
+ }
+
+ const dataTransfer =
+ "dataTransfer" in event ? event.dataTransfer : event.clipboardData;
+ if (dataTransfer === null) {
+ return;
+ }
+
+ let format: (typeof acceptedMIMETypes)[number] | null = null;
+ for (const mimeType of acceptedMIMETypes) {
+ if (dataTransfer.types.includes(mimeType)) {
+ format = mimeType;
+ break;
+ }
+ }
+ if (format !== "Files") {
+ return;
+ }
+
+ const items = dataTransfer.items;
+ if (!items) {
+ return;
+ }
+
+ event.preventDefault();
+
+ for (let i = 0; i < items.length; i++) {
+ // Gets file block corresponding to MIME type.
+ let fileBlockType = "file";
+ for (const blockSpec of Object.values(editor.schema.blockSpecs)) {
+ for (const mimeType of blockSpec.implementation.meta?.fileBlockAccept ||
+ []) {
+ const isFileExtension = mimeType.startsWith(".");
+ const file = items[i].getAsFile();
+
+ if (file) {
+ if (
+ (!isFileExtension &&
+ file.type &&
+ checkMIMETypesMatch(items[i].type, mimeType)) ||
+ (isFileExtension &&
+ checkFileExtensionsMatch(
+ "." + file.name.split(".").pop(),
+ mimeType,
+ ))
+ ) {
+ fileBlockType = blockSpec.config.type;
+ break;
+ }
+ }
+ }
+ }
+
+ const file = items[i].getAsFile();
+ if (file) {
+ const fileBlock = {
+ type: fileBlockType,
+ props: {
+ name: file.name,
+ },
+ } as PartialBlock;
+
+ let insertedBlockId: string | undefined = undefined;
+
+ if (event.type === "paste") {
+ const currentBlock = editor.getTextCursorPosition().block;
+ insertedBlockId = insertOrUpdateBlock(editor, currentBlock, fileBlock);
+ } else if (event.type === "drop") {
+ const coords = {
+ left: (event as DragEvent).clientX,
+ top: (event as DragEvent).clientY,
+ };
+
+ const pos = editor.prosemirrorView.posAtCoords(coords);
+
+ if (!pos) {
+ return;
+ }
+
+ insertedBlockId = editor.transact((tr) => {
+ const posInfo = getNearestBlockPos(tr.doc, pos.pos);
+ const blockElement = editor.domElement?.querySelector(
+ `[data-id="${posInfo.node.attrs.id}"]`,
+ );
+
+ const blockRect = blockElement?.getBoundingClientRect();
+
+ return insertOrUpdateBlock(
+ editor,
+ editor.getBlock(posInfo.node.attrs.id)!,
+ fileBlock,
+ blockRect && (blockRect.top + blockRect.bottom) / 2 > coords.top
+ ? "before"
+ : "after",
+ );
+ });
+ } else {
+ return;
+ }
+
+ const updateData = await editor.uploadFile(file, insertedBlockId);
+
+ const updatedFileBlock =
+ typeof updateData === "string"
+ ? ({
+ props: {
+ url: updateData,
+ },
+ } as PartialBlock)
+ : { ...updateData };
+
+ editor.updateBlock(insertedBlockId, updatedFileBlock);
+ }
+ }
+}
diff --git a/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts
new file mode 100644
index 0000000000..ffb298544f
--- /dev/null
+++ b/packages/core/src/api/clipboard/fromClipboard/handleVSCodePaste.ts
@@ -0,0 +1,38 @@
+import { EditorView } from "prosemirror-view";
+
+export function handleVSCodePaste(event: ClipboardEvent, view: EditorView) {
+ const { schema } = view.state;
+
+ if (!event.clipboardData) {
+ return false;
+ }
+
+ const text = event.clipboardData!.getData("text/plain");
+
+ if (!text) {
+ return false;
+ }
+
+ if (!schema.nodes.codeBlock) {
+ return false;
+ }
+
+ const vscode = event.clipboardData!.getData("vscode-editor-data");
+ const vscodeData = vscode ? JSON.parse(vscode) : undefined;
+ const language = vscodeData?.mode;
+
+ if (!language) {
+ return false;
+ }
+
+ // strip carriage return chars from text pasted as code
+ // see: https://github.com/ProseMirror/prosemirror-view/commit/a50a6bcceb4ce52ac8fcc6162488d8875613aacd
+ view.pasteHTML(
+ `
${text.replace(
+ /\r\n?/g,
+ "\n",
+ )}
`,
+ );
+
+ return true;
+}
diff --git a/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
new file mode 100644
index 0000000000..9fa4ed3c55
--- /dev/null
+++ b/packages/core/src/api/clipboard/fromClipboard/pasteExtension.ts
@@ -0,0 +1,157 @@
+import { Extension } from "@tiptap/core";
+import { Plugin } from "prosemirror-state";
+
+import type {
+ BlockNoteEditor,
+ BlockNoteEditorOptions,
+} from "../../../editor/BlockNoteEditor";
+import { isMarkdown } from "../../parsers/markdown/detectMarkdown.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import { acceptedMIMETypes } from "./acceptedMIMETypes.js";
+import { handleFileInsertion } from "./handleFileInsertion.js";
+import { handleVSCodePaste } from "./handleVSCodePaste.js";
+
+function defaultPasteHandler({
+ event,
+ editor,
+ prioritizeMarkdownOverHTML,
+ plainTextAsMarkdown,
+}: {
+ event: ClipboardEvent;
+ editor: BlockNoteEditor;
+ prioritizeMarkdownOverHTML: boolean;
+ plainTextAsMarkdown: boolean;
+}) {
+ // Special case for code blocks, as they do not support any rich text
+ // formatting, so we force pasting plain text.
+ const isInCodeBlock = editor.transact(
+ (tr) =>
+ tr.selection.$from.parent.type.spec.code &&
+ tr.selection.$to.parent.type.spec.code,
+ );
+
+ if (isInCodeBlock) {
+ const data = event.clipboardData?.getData("text/plain");
+
+ if (data) {
+ editor.pasteText(data);
+
+ return true;
+ }
+ }
+
+ let format: (typeof acceptedMIMETypes)[number] | undefined;
+ for (const mimeType of acceptedMIMETypes) {
+ if (event.clipboardData!.types.includes(mimeType)) {
+ format = mimeType;
+ break;
+ }
+ }
+
+ if (!format) {
+ return true;
+ }
+
+ if (format === "vscode-editor-data") {
+ // If VSCode clipboard data cannot be parsed as a code block, try parsing
+ // `text/plain` as a fallback.
+ if (handleVSCodePaste(event, editor.prosemirrorView)) {
+ return true;
+ }
+
+ format = "text/plain";
+ }
+
+ if (format === "Files") {
+ handleFileInsertion(event, editor);
+ return true;
+ }
+
+ const data = event.clipboardData!.getData(format);
+
+ if (format === "blocknote/html") {
+ // Is blocknote/html, so no need to convert it
+ editor.pasteHTML(data, true);
+ return true;
+ }
+
+ if (format === "text/markdown") {
+ editor.pasteMarkdown(data);
+ return true;
+ }
+
+ if (prioritizeMarkdownOverHTML) {
+ // Use plain text instead of HTML if it looks like Markdown
+ const plainText = event.clipboardData!.getData("text/plain");
+
+ if (isMarkdown(plainText)) {
+ editor.pasteMarkdown(plainText);
+ return true;
+ }
+ }
+
+ if (format === "text/html") {
+ editor.pasteHTML(data);
+ return true;
+ }
+
+ if (plainTextAsMarkdown) {
+ editor.pasteMarkdown(data);
+ return true;
+ }
+
+ editor.pasteText(data);
+ return true;
+}
+
+export const createPasteFromClipboardExtension = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ pasteHandler: Exclude<
+ BlockNoteEditorOptions["pasteHandler"],
+ undefined
+ >,
+) =>
+ Extension.create({
+ name: "pasteFromClipboard",
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ props: {
+ handleDOMEvents: {
+ paste(_view, event) {
+ event.preventDefault();
+
+ if (!editor.isEditable) {
+ return;
+ }
+
+ return pasteHandler({
+ event,
+ editor,
+ defaultPasteHandler: ({
+ prioritizeMarkdownOverHTML = true,
+ plainTextAsMarkdown = true,
+ } = {}) => {
+ return defaultPasteHandler({
+ event,
+ editor,
+ prioritizeMarkdownOverHTML,
+ plainTextAsMarkdown,
+ });
+ },
+ });
+ },
+ },
+ },
+ }),
+ ];
+ },
+ });
diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
new file mode 100644
index 0000000000..e150af1309
--- /dev/null
+++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts
@@ -0,0 +1,299 @@
+import { Extension } from "@tiptap/core";
+import { Fragment, Node } from "prosemirror-model";
+import { NodeSelection, Plugin } from "prosemirror-state";
+import { CellSelection } from "prosemirror-tables";
+import type { EditorView } from "prosemirror-view";
+
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import { createExternalHTMLExporter } from "../../exporters/html/externalHTMLExporter.js";
+import { cleanHTMLToMarkdown } from "../../exporters/markdown/markdownExporter.js";
+import { fragmentToBlocks } from "../../nodeConversions/fragmentToBlocks.js";
+import {
+ contentNodeToInlineContent,
+ contentNodeToTableContent,
+} from "../../nodeConversions/nodeToBlock.js";
+
+function fragmentToExternalHTML<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ view: EditorView,
+ selectedFragment: Fragment,
+ editor: BlockNoteEditor,
+) {
+ let isWithinBlockContent = false;
+ const isWithinTable = view.state.selection instanceof CellSelection;
+
+ if (!isWithinTable) {
+ // Checks whether block ancestry should be included when creating external
+ // HTML. If the selection is within a block content node, the block ancestry
+ // is excluded as we only care about the inline content.
+ const fragmentWithoutParents = view.state.doc.slice(
+ view.state.selection.from,
+ view.state.selection.to,
+ false,
+ ).content;
+
+ const children = [];
+ for (let i = 0; i < fragmentWithoutParents.childCount; i++) {
+ children.push(fragmentWithoutParents.child(i));
+ }
+
+ isWithinBlockContent =
+ children.find(
+ (child) =>
+ child.type.isInGroup("bnBlock") ||
+ child.type.name === "blockGroup" ||
+ child.type.spec.group === "blockContent",
+ ) === undefined;
+ if (isWithinBlockContent) {
+ selectedFragment = fragmentWithoutParents;
+ }
+ }
+
+ let externalHTML: string;
+
+ const externalHTMLExporter = createExternalHTMLExporter(
+ view.state.schema,
+ editor,
+ );
+
+ if (isWithinTable) {
+ if (selectedFragment.firstChild?.type.name === "table") {
+ // contentNodeToTableContent expects the fragment of the content of a table, not the table node itself
+ // but cellselection.content() returns the table node itself if all cells and columns are selected
+ selectedFragment = selectedFragment.firstChild.content;
+ }
+
+ // first convert selection to blocknote-style table content, and then
+ // pass this to the exporter
+ const ic = contentNodeToTableContent(
+ selectedFragment as any,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema,
+ );
+
+ // Wrap in table to ensure correct parsing by spreadsheet applications
+ externalHTML = `
${externalHTMLExporter.exportInlineContent(
+ ic as any,
+ {},
+ )}
`;
+ } else if (isWithinBlockContent) {
+ // first convert selection to blocknote-style inline content, and then
+ // pass this to the exporter
+ const ic = contentNodeToInlineContent(
+ selectedFragment as any,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema,
+ );
+ externalHTML = externalHTMLExporter.exportInlineContent(ic, {});
+ } else {
+ const blocks = fragmentToBlocks(selectedFragment);
+ externalHTML = externalHTMLExporter.exportBlocks(blocks, {});
+ }
+ return externalHTML;
+}
+
+export function selectedFragmentToHTML<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ view: EditorView,
+ editor: BlockNoteEditor,
+): {
+ clipboardHTML: string;
+ externalHTML: string;
+ markdown: string;
+} {
+ // Checks if a `blockContent` node is being copied and expands
+ // the selection to the parent `blockContainer` node. This is
+ // for the use-case in which only a block without content is
+ // selected, e.g. an image block.
+ if (
+ "node" in view.state.selection &&
+ (view.state.selection.node as Node).type.spec.group === "blockContent"
+ ) {
+ editor.transact((tr) =>
+ tr.setSelection(
+ new NodeSelection(tr.doc.resolve(view.state.selection.from - 1)),
+ ),
+ );
+ }
+
+ // Uses default ProseMirror clipboard serialization.
+ const clipboardHTML: string = view.serializeForClipboard(
+ view.state.selection.content(),
+ ).dom.innerHTML;
+
+ const selectedFragment = view.state.selection.content().content;
+
+ const externalHTML = fragmentToExternalHTML(
+ view,
+ selectedFragment,
+ editor,
+ );
+
+ // Code blocks are treated differently for copying: text/plain is the raw
+ // selected text instead of markdown.
+ const { $from, $to } = view.state.selection;
+ const parentBlockType = $from.parent.type.name;
+ const parentBlockSpec = editor.blockImplementations[parentBlockType as any];
+ const isPurelyInsideCodeBlock =
+ $from.sameParent($to) &&
+ parentBlockSpec?.implementation.meta?.code === true;
+
+ const markdown = isPurelyInsideCodeBlock
+ ? view.state.doc.textBetween($from.pos, $to.pos)
+ : cleanHTMLToMarkdown(externalHTML);
+
+ return { clipboardHTML, externalHTML, markdown };
+}
+
+const checkIfSelectionInNonEditableBlock = (view: EditorView) => {
+ // Use ProseMirror's internal selection state to check for empty selection.
+ // window.getSelection() returns null or a collapsed selection inside Shadow
+ // DOM (Firefox, Safari, and Chromium edge cases), causing this guard to
+ // misfire and silently skip clipboard writes. view.state.selection is always
+ // accurate regardless of DOM mode.
+ if (view.state.selection.empty) {
+ return true;
+ }
+
+ // Let browser handle event if it's within a non-editable
+ // "island". This means it's in selectable content within a
+ // non-editable block. We only need to check one node as it's
+ // not possible for the browser selection to start in an
+ // editable block and end in a non-editable one.
+ const selection = window.getSelection();
+ if (selection && !selection.isCollapsed) {
+ let node = selection.focusNode;
+ while (node) {
+ if (
+ node instanceof HTMLElement &&
+ node.getAttribute("contenteditable") === "false"
+ ) {
+ return true;
+ }
+
+ node = node.parentElement;
+ }
+ }
+
+ return false;
+};
+
+const copyToClipboard = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ view: EditorView,
+ event: ClipboardEvent,
+) => {
+ // Stops the default browser copy behaviour.
+ event.preventDefault();
+ event.clipboardData!.clearData();
+
+ const { clipboardHTML, externalHTML, markdown } = selectedFragmentToHTML(
+ view,
+ editor,
+ );
+
+ // TODO: Writing to other MIME types not working in Safari for
+ // some reason.
+ event.clipboardData!.setData("blocknote/html", clipboardHTML);
+ event.clipboardData!.setData("text/html", externalHTML);
+ event.clipboardData!.setData("text/plain", markdown);
+};
+
+export const createCopyToClipboardExtension = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+) =>
+ Extension.create<{ editor: BlockNoteEditor }, undefined>({
+ name: "copyToClipboard",
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ props: {
+ handleDOMEvents: {
+ copy(view, event) {
+ if (checkIfSelectionInNonEditableBlock(view)) {
+ return true;
+ }
+
+ copyToClipboard(editor, view, event);
+ // Prevent default PM handler to be called
+ return true;
+ },
+ cut(view, event) {
+ if (checkIfSelectionInNonEditableBlock(view)) {
+ return true;
+ }
+
+ copyToClipboard(editor, view, event);
+ if (view.editable) {
+ view.dispatch(view.state.tr.deleteSelection());
+ }
+ // Prevent default PM handler to be called
+ return true;
+ },
+ // This is for the use-case in which only a block without content
+ // is selected, e.g. an image block, and dragged (not using the
+ // drag handle).
+ dragstart(view, event) {
+ // Checks if a `NodeSelection` is active.
+ if (!("node" in view.state.selection)) {
+ return;
+ }
+
+ // Checks if a `blockContent` node is being dragged.
+ if (
+ (view.state.selection.node as Node).type.spec.group !==
+ "blockContent"
+ ) {
+ return;
+ }
+
+ // Expands the selection to the parent `blockContainer` node.
+ editor.transact((tr) =>
+ tr.setSelection(
+ new NodeSelection(
+ tr.doc.resolve(view.state.selection.from - 1),
+ ),
+ ),
+ );
+
+ // Stops the default browser drag start behaviour.
+ event.preventDefault();
+ event.dataTransfer!.clearData();
+
+ const { clipboardHTML, externalHTML, markdown } =
+ selectedFragmentToHTML(view, editor);
+
+ // TODO: Writing to other MIME types not working in Safari for
+ // some reason.
+ event.dataTransfer!.setData("blocknote/html", clipboardHTML);
+ event.dataTransfer!.setData("text/html", externalHTML);
+ event.dataTransfer!.setData("text/plain", markdown);
+
+ // Prevent default PM handler to be called
+ return true;
+ },
+ },
+ },
+ }),
+ ];
+ },
+ });
diff --git a/packages/core/src/api/exporters/html/externalHTMLExporter.ts b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
new file mode 100644
index 0000000000..2149c884e7
--- /dev/null
+++ b/packages/core/src/api/exporters/html/externalHTMLExporter.ts
@@ -0,0 +1,76 @@
+import { DOMSerializer, Schema } from "prosemirror-model";
+
+import { PartialBlock } from "../../../blocks/defaultBlocks.js";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import {
+ BlockSchema,
+ InlineContent,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import {
+ serializeBlocksExternalHTML,
+ serializeInlineContentExternalHTML,
+} from "./util/serializeBlocksExternalHTML.js";
+
+// Used to export BlockNote blocks and ProseMirror nodes to HTML for use outside
+// the editor. Blocks are exported using the `toExternalHTML` method in their
+// `blockSpec`, or `toInternalHTML` if `toExternalHTML` is not defined.
+//
+// The HTML created by this serializer is different to what's rendered by the
+// editor to the DOM. This also means that data is likely to be lost when
+// converting back to original blocks. The differences in the output HTML are:
+// 1. It doesn't include the `blockGroup` and `blockContainer` wrappers meaning
+// that nesting is not preserved for non-list-item blocks.
+// 2. `li` items in the output HTML are wrapped in `ul` or `ol` elements.
+// 3. While nesting for list items is preserved, other types of blocks nested
+// inside a list are un-nested and a new list is created after them.
+// 4. The HTML is wrapped in a single `div` element.
+
+// Needs to be sync because it's used in drag handler event (SideMenuPlugin)
+export const createExternalHTMLExporter = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ schema: Schema,
+ editor: BlockNoteEditor,
+) => {
+ const serializer = DOMSerializer.fromSchema(schema);
+
+ return {
+ exportBlocks: (
+ blocks: PartialBlock[],
+ options: { document?: Document },
+ ) => {
+ const html = serializeBlocksExternalHTML(
+ editor,
+ blocks,
+ serializer,
+ new Set(["numberedListItem"]),
+ new Set(["bulletListItem", "checkListItem", "toggleListItem"]),
+ options,
+ );
+ const div = document.createElement("div");
+ div.append(html);
+ return div.innerHTML;
+ },
+
+ exportInlineContent: (
+ inlineContent: InlineContent[],
+ options: { document?: Document },
+ ) => {
+ const domFragment = serializeInlineContentExternalHTML(
+ editor,
+ inlineContent as any,
+ serializer,
+ options,
+ );
+
+ const parent = document.createElement("div");
+ parent.append(domFragment.cloneNode(true));
+
+ return parent.innerHTML;
+ },
+ };
+};
diff --git a/packages/core/src/api/exporters/html/internalHTMLSerializer.ts b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
new file mode 100644
index 0000000000..33376b2835
--- /dev/null
+++ b/packages/core/src/api/exporters/html/internalHTMLSerializer.ts
@@ -0,0 +1,182 @@
+import { DOMSerializer, Schema } from "prosemirror-model";
+
+import { PartialBlock } from "../../../blocks/defaultBlocks.js";
+import { EMPTY_CELL_WIDTH } from "../../../blocks/index.js";
+import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../schema/index.js";
+import { serializeBlocksInternalHTML } from "./util/serializeBlocksInternalHTML.js";
+
+// This is normally handled using decorations in the
+// `NumberedListIndexingDecorationPlugin`. This does not run when exporting, so
+// we have to add the necessary HTML attributes ourselves.
+const addIndexToNumberedListItems = (element: HTMLElement) => {
+ const numberedListItems = element.querySelectorAll(
+ '[data-content-type="numberedListItem"]',
+ );
+ numberedListItems.forEach((numberedListItem) => {
+ const prevNumberedListItem = numberedListItem
+ .closest(".bn-block-outer")
+ ?.previousElementSibling?.querySelector(
+ '[data-content-type="numberedListItem"]',
+ );
+
+ if (!prevNumberedListItem) {
+ numberedListItem.setAttribute(
+ "data-index",
+ numberedListItem.getAttribute("data-start") || "1",
+ );
+ } else {
+ const prevNumberedListItemIndex =
+ prevNumberedListItem.getAttribute("data-index");
+ numberedListItem.setAttribute(
+ "data-index",
+ (parseInt(prevNumberedListItemIndex || "0") + 1).toString(),
+ );
+ }
+ });
+
+ return element;
+};
+
+// Makes the checkboxes in check list items read-only, as the HTML should be
+// static and therefore read-only when rendered.
+const makeCheckListItemsReadOnly = (element: HTMLElement) => {
+ const checkboxes: NodeListOf = element.querySelectorAll(
+ '[data-content-type="checkListItem"] input',
+ );
+ checkboxes.forEach((checkbox) => {
+ checkbox.disabled = true;
+ });
+
+ return element;
+};
+
+// Forces toggle blocks (toggle headings, toggle list items) to be expanded.
+// This is because event listeners for the toggle button are lost when
+// serializing HTML elements to a string, so the button no longer works if the
+// HTML string is rendered out.
+const forceToggleBlocksShow = (element: HTMLElement) => {
+ const hiddenToggleWrappers = element.querySelectorAll(
+ '.bn-toggle-wrapper[data-show-children="false"]',
+ );
+ hiddenToggleWrappers.forEach((toggleWrapper) => {
+ toggleWrapper.setAttribute("data-show-children", "true");
+ });
+
+ return element;
+};
+
+// Adds minimum cell widths, which would normally be done by the
+// `columnResizing` extension. This extension doesn't run when exporting to
+// HTML, so we have to add this manually.
+const addTableMinCellWidths = (element: HTMLElement) => {
+ const tables = element.querySelectorAll('[data-content-type="table"] table');
+ tables.forEach((table) => {
+ table.setAttribute(
+ "style",
+ `--default-cell-min-width: ${EMPTY_CELL_WIDTH}px;`,
+ );
+ table.setAttribute("data-show-children", "true");
+ });
+
+ return element;
+};
+
+// Adds table wrapping elements, which would normally be done by the
+// `columnResizing` extension. This extension doesn't run when exporting to
+// HTML, so we have to add this manually. This adds the correct padding to
+// tables.
+const addTableWrappers = (element: HTMLElement) => {
+ const tables = element.querySelectorAll('[data-content-type="table"] table');
+ tables.forEach((table) => {
+ const tableWrapper = document.createElement("div");
+ tableWrapper.className = "tableWrapper";
+ const tableWrapperInner = document.createElement("div");
+ tableWrapperInner.className = "tableWrapper-inner";
+
+ tableWrapper.appendChild(tableWrapperInner);
+ table.parentElement?.appendChild(tableWrapper);
+ tableWrapper.appendChild(table);
+ });
+
+ return element;
+};
+
+// Adds trailing breaks to blocks with empty inline content. This is normally
+// done by ProseMirror, but only when rendering an actual editor. Without them,
+// empty inline content has a height of 0.
+const addTrailingBreakToEmptyInlineContent = (element: HTMLElement) => {
+ const emptyInlineContent = element.querySelectorAll(
+ ".bn-inline-content:empty",
+ );
+ emptyInlineContent.forEach((inlineContent) => {
+ // We actually use a `span` instead of a `br` to avoid potential false
+ // positives when parsing.
+ const trailingBreak = document.createElement("span");
+ trailingBreak.className = "ProseMirror-trailingBreak";
+ trailingBreak.setAttribute("style", "display: inline-block;");
+
+ inlineContent.appendChild(trailingBreak);
+ });
+
+ return element;
+};
+
+// Used to serialize BlockNote blocks and ProseMirror nodes to HTML without
+// losing data. Blocks are exported using the `toInternalHTML` method in their
+// `blockSpec`.
+//
+// The HTML created by this serializer is the same as what's rendered by the
+// editor to the DOM. This means that it retains the same structure as the
+// editor, including the `blockGroup` and `blockContainer` wrappers. This also
+// means that it can be converted back to the original blocks without any data
+// loss.
+export const createInternalHTMLSerializer = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ schema: Schema,
+ editor: BlockNoteEditor,
+) => {
+ const serializer = DOMSerializer.fromSchema(schema);
+
+ // Set of transforms to run on the output HTML element after serializing
+ // blocks. These are used to add HTML elements, attributes, or class names
+ // which would normally be done by extensions and plugins. Since these don't
+ // run when converting blocks to HTML, tranforms are used to mock their
+ // functionality so that the rendered HTML looks identical to that of a live
+ // editor.
+ const transforms: ((element: HTMLElement) => HTMLElement)[] = [
+ addIndexToNumberedListItems,
+ makeCheckListItemsReadOnly,
+ forceToggleBlocksShow,
+ addTableMinCellWidths,
+ addTableWrappers,
+ addTrailingBreakToEmptyInlineContent,
+ ];
+
+ return {
+ serializeBlocks: (
+ blocks: PartialBlock[],
+ options: { document?: Document },
+ ) => {
+ let element = serializeBlocksInternalHTML(
+ editor,
+ blocks,
+ serializer,
+ options,
+ );
+
+ for (const transform of transforms) {
+ element = transform(element);
+ }
+
+ return element.outerHTML;
+ },
+ };
+};
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
new file mode 100644
index 0000000000..72569ffced
--- /dev/null
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts
@@ -0,0 +1,400 @@
+import { DOMSerializer, Fragment, Node } from "prosemirror-model";
+
+import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
+import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
+import {
+ BlockImplementation,
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../../schema/index.js";
+import { UnreachableCaseError } from "../../../../util/typescript.js";
+import {
+ inlineContentToNodes,
+ tableContentToNodes,
+} from "../../../nodeConversions/blockToNode.js";
+import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js";
+
+function addAttributesAndRemoveClasses(element: HTMLElement) {
+ // Removes all BlockNote specific class names.
+ const className =
+ Array.from(element.classList).filter(
+ (className) => !className.startsWith("bn-"),
+ ) || [];
+
+ if (className.length > 0) {
+ element.className = className.join(" ");
+ } else {
+ element.removeAttribute("class");
+ }
+}
+
+export function serializeInlineContentExternalHTML<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blockContent: PartialBlock["content"],
+ serializer: DOMSerializer,
+ options?: { document?: Document; blockType?: string },
+) {
+ let nodes: Node[];
+
+ // TODO: reuse function from nodeconversions?
+ if (!blockContent) {
+ throw new Error("blockContent is required");
+ } else if (typeof blockContent === "string") {
+ nodes = inlineContentToNodes(
+ [blockContent],
+ editor.pmSchema,
+ options?.blockType,
+ );
+ } else if (Array.isArray(blockContent)) {
+ nodes = inlineContentToNodes(
+ blockContent,
+ editor.pmSchema,
+ options?.blockType,
+ );
+ } else if (blockContent.type === "tableContent") {
+ nodes = tableContentToNodes(blockContent, editor.pmSchema);
+ } else {
+ throw new UnreachableCaseError(blockContent.type);
+ }
+
+ // Check if any of the nodes are custom inline content with toExternalHTML
+ const doc = options?.document ?? document;
+ const fragment = doc.createDocumentFragment();
+
+ for (const node of nodes) {
+ // Check if this is a custom inline content node with toExternalHTML
+ if (
+ node.type.name !== "text" &&
+ editor.schema.inlineContentSchema[node.type.name]
+ ) {
+ const inlineContentImplementation =
+ editor.schema.inlineContentSpecs[node.type.name].implementation;
+
+ if (inlineContentImplementation) {
+ // Convert the node to inline content format
+ const inlineContent = nodeToCustomInlineContent(
+ node,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema,
+ );
+
+ // Use the custom toExternalHTML method or fallback to `render`
+ const output = inlineContentImplementation.toExternalHTML
+ ? inlineContentImplementation.toExternalHTML(
+ inlineContent as any,
+ editor as any,
+ )
+ : inlineContentImplementation.render.call(
+ {
+ renderType: "dom",
+ props: undefined,
+ },
+ inlineContent as any,
+ () => {
+ // No-op
+ },
+ editor as any,
+ );
+
+ if (output) {
+ fragment.appendChild(output.dom);
+
+ // If contentDOM exists, render the inline content into it
+ if (output.contentDOM) {
+ const contentFragment = serializer.serializeFragment(
+ node.content,
+ options,
+ );
+ output.contentDOM.dataset.editable = "";
+ output.contentDOM.appendChild(contentFragment);
+ }
+ continue;
+ }
+ }
+ } else if (node.type.name === "text") {
+ // We serialize text nodes manually as we need to serialize the styles/
+ // marks using `styleSpec.implementation.render`. When left up to
+ // ProseMirror, it'll use `toDOM` which is incorrect.
+ let dom: globalThis.Node | Text = document.createTextNode(
+ node.textContent,
+ );
+ // Reverse the order of marks to maintain the correct priority.
+ for (const mark of node.marks.toReversed()) {
+ if (mark.type.name in editor.schema.styleSpecs) {
+ const newDom = (
+ editor.schema.styleSpecs[mark.type.name].implementation
+ .toExternalHTML ??
+ editor.schema.styleSpecs[mark.type.name].implementation.render
+ )(mark.attrs["stringValue"], editor);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ } else {
+ const domOutputSpec = mark.type.spec.toDOM!(mark, true);
+ const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ }
+ }
+
+ fragment.appendChild(dom);
+ } else {
+ // Fall back to default serialization for this node
+ const nodeFragment = serializer.serializeFragment(
+ Fragment.from([node]),
+ options,
+ );
+ fragment.appendChild(nodeFragment);
+ }
+ }
+
+ if (
+ fragment.childNodes.length === 1 &&
+ fragment.firstChild?.nodeType === 1 /* Node.ELEMENT_NODE */
+ ) {
+ addAttributesAndRemoveClasses(fragment.firstChild as HTMLElement);
+ }
+
+ return fragment;
+}
+
+/**
+ * TODO: there's still quite some logic that handles getting and filtering properties,
+ * we should make sure the `toExternalHTML` methods of default blocks actually handle this,
+ * instead of the serializer.
+ */
+function serializeBlock<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ fragment: DocumentFragment,
+ editor: BlockNoteEditor,
+ block: PartialBlock,
+ serializer: DOMSerializer,
+ orderedListItemBlockTypes: Set,
+ unorderedListItemBlockTypes: Set,
+ nestingLevel: number,
+ options?: { document?: Document },
+) {
+ const doc = options?.document ?? document;
+ const BC_NODE = editor.pmSchema.nodes["blockContainer"];
+
+ // set default props in case we were passed a partial block
+ const props = block.props || {};
+ for (const [name, spec] of Object.entries(
+ editor.schema.blockSchema[block.type as any].propSchema,
+ )) {
+ if (!(name in props) && spec.default !== undefined) {
+ (props as any)[name] = spec.default;
+ }
+ }
+
+ const bc = BC_NODE.spec?.toDOM?.(
+ BC_NODE.create({
+ id: block.id,
+ ...props,
+ }),
+ ) as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+
+ // the container node is just used as a workaround to get some block-level attributes.
+ // we should change toExternalHTML so that this is not necessary
+ const attrs = Array.from(bc.dom.attributes);
+
+ const blockImplementation = editor.blockImplementations[block.type as any]
+ .implementation as BlockImplementation;
+ const ret =
+ blockImplementation.toExternalHTML?.call(
+ {},
+ { ...block, props } as any,
+ editor as any,
+ {
+ nestingLevel,
+ },
+ ) ||
+ blockImplementation.render.call(
+ {},
+ { ...block, props } as any,
+ editor as any,
+ );
+
+ const elementFragment = doc.createDocumentFragment();
+
+ if ((ret.dom as HTMLElement).classList.contains("bn-block-content")) {
+ const blockContentDataAttributes = [
+ ...attrs,
+ ...Array.from((ret.dom as HTMLElement).attributes),
+ ].filter(
+ (attr) =>
+ attr.name.startsWith("data") &&
+ attr.name !== "data-content-type" &&
+ attr.name !== "data-file-block" &&
+ attr.name !== "data-node-view-wrapper" &&
+ attr.name !== "data-node-type" &&
+ attr.name !== "data-id" &&
+ attr.name !== "data-editable",
+ );
+
+ // ret.dom = ret.dom.firstChild! as any;
+ for (const attr of blockContentDataAttributes) {
+ (ret.dom.firstChild! as HTMLElement).setAttribute(attr.name, attr.value);
+ }
+
+ addAttributesAndRemoveClasses(ret.dom.firstChild! as HTMLElement);
+ if (nestingLevel > 0) {
+ (ret.dom.firstChild! as HTMLElement).setAttribute(
+ "data-nesting-level",
+ nestingLevel.toString(),
+ );
+ }
+ elementFragment.append(...Array.from(ret.dom.childNodes));
+ } else {
+ elementFragment.append(ret.dom);
+ if (nestingLevel > 0) {
+ (ret.dom as HTMLElement).setAttribute(
+ "data-nesting-level",
+ nestingLevel.toString(),
+ );
+ }
+ }
+
+ if (ret.contentDOM && block.content) {
+ const ic = serializeInlineContentExternalHTML(
+ editor,
+ block.content as any, // TODO
+ serializer,
+ { ...options, blockType: block.type },
+ );
+
+ ret.contentDOM.appendChild(ic);
+ }
+
+ let listType = undefined;
+ if (orderedListItemBlockTypes.has(block.type!)) {
+ listType = "OL";
+ } else if (unorderedListItemBlockTypes.has(block.type!)) {
+ listType = "UL";
+ }
+
+ if (listType) {
+ if (fragment.lastChild?.nodeName !== listType) {
+ const list = doc.createElement(listType);
+
+ if (
+ listType === "OL" &&
+ "start" in props &&
+ props.start &&
+ props?.start !== 1
+ ) {
+ list.setAttribute("start", props.start + "");
+ }
+ fragment.append(list);
+ }
+ fragment.lastChild!.appendChild(elementFragment);
+ } else {
+ fragment.append(elementFragment);
+ }
+
+ if (block.children && block.children.length > 0) {
+ const childFragment = doc.createDocumentFragment();
+ serializeBlocksToFragment(
+ childFragment,
+ editor,
+ block.children,
+ serializer,
+ orderedListItemBlockTypes,
+ unorderedListItemBlockTypes,
+ nestingLevel + 1,
+ options,
+ );
+ if (
+ fragment.lastChild?.nodeName === "UL" ||
+ fragment.lastChild?.nodeName === "OL"
+ ) {
+ // add nested lists to the last list item
+ while (
+ childFragment.firstChild?.nodeName === "UL" ||
+ childFragment.firstChild?.nodeName === "OL"
+ ) {
+ fragment.lastChild!.lastChild!.appendChild(childFragment.firstChild!);
+ }
+ }
+
+ if ("childrenDOM" in ret && ret.childrenDOM) {
+ // block specifies where children should go (e.g. toggle blocks
+ // place children inside )
+ ret.childrenDOM.append(childFragment);
+ } else if (
+ editor.pmSchema.nodes[block.type as any].isInGroup("blockContent")
+ ) {
+ // default "blockContainer" style blocks are flattened (no "nested block" support) for externalHTML, so append the child fragment to the outer fragment
+ fragment.append(childFragment);
+ } else {
+ // for columns / column lists, do use nesting
+ ret.contentDOM?.append(childFragment);
+ }
+ }
+}
+
+const serializeBlocksToFragment = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ fragment: DocumentFragment,
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
+ serializer: DOMSerializer,
+ orderedListItemBlockTypes: Set,
+ unorderedListItemBlockTypes: Set,
+ nestingLevel = 0,
+ options?: { document?: Document },
+) => {
+ for (const block of blocks) {
+ serializeBlock(
+ fragment,
+ editor,
+ block,
+ serializer,
+ orderedListItemBlockTypes,
+ unorderedListItemBlockTypes,
+ nestingLevel,
+ options,
+ );
+ }
+};
+
+export const serializeBlocksExternalHTML = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
+ serializer: DOMSerializer,
+ orderedListItemBlockTypes: Set,
+ unorderedListItemBlockTypes: Set,
+ options?: { document?: Document },
+) => {
+ const doc = options?.document ?? document;
+ const fragment = doc.createDocumentFragment();
+
+ serializeBlocksToFragment(
+ fragment,
+ editor,
+ blocks,
+ serializer,
+ orderedListItemBlockTypes,
+ unorderedListItemBlockTypes,
+ 0,
+ options,
+ );
+ return fragment;
+};
diff --git a/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
new file mode 100644
index 0000000000..0f890b77ab
--- /dev/null
+++ b/packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts
@@ -0,0 +1,253 @@
+import { DOMSerializer, Fragment, Node } from "prosemirror-model";
+
+import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
+import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
+import {
+ BlockSchema,
+ InlineContentSchema,
+ StyleSchema,
+} from "../../../../schema/index.js";
+import { UnreachableCaseError } from "../../../../util/typescript.js";
+import {
+ inlineContentToNodes,
+ tableContentToNodes,
+} from "../../../nodeConversions/blockToNode.js";
+
+import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js";
+export function serializeInlineContentInternalHTML<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blockContent: PartialBlock["content"],
+ serializer: DOMSerializer,
+ blockType?: string,
+ options?: { document?: Document },
+) {
+ let nodes: Node[];
+
+ // TODO: reuse function from nodeconversions?
+ if (!blockContent) {
+ throw new Error("blockContent is required");
+ } else if (typeof blockContent === "string") {
+ nodes = inlineContentToNodes([blockContent], editor.pmSchema, blockType);
+ } else if (Array.isArray(blockContent)) {
+ nodes = inlineContentToNodes(blockContent, editor.pmSchema, blockType);
+ } else if (blockContent.type === "tableContent") {
+ nodes = tableContentToNodes(blockContent, editor.pmSchema);
+ } else {
+ throw new UnreachableCaseError(blockContent.type);
+ }
+
+ // Check if any of the nodes are custom inline content with toExternalHTML
+ const doc = options?.document ?? document;
+ const fragment = doc.createDocumentFragment();
+
+ for (const node of nodes) {
+ // Check if this is a custom inline content node with toExternalHTML
+ if (
+ node.type.name !== "text" &&
+ editor.schema.inlineContentSchema[node.type.name]
+ ) {
+ const inlineContentImplementation =
+ editor.schema.inlineContentSpecs[node.type.name].implementation;
+
+ if (inlineContentImplementation) {
+ // Convert the node to inline content format
+ const inlineContent = nodeToCustomInlineContent(
+ node,
+ editor.schema.inlineContentSchema,
+ editor.schema.styleSchema,
+ );
+
+ // Use the custom toExternalHTML method
+ const output = inlineContentImplementation.render.call(
+ {
+ renderType: "dom",
+ props: undefined,
+ },
+ inlineContent as any,
+ () => {
+ // No-op
+ },
+ editor as any,
+ );
+
+ if (output) {
+ fragment.appendChild(output.dom);
+
+ // If contentDOM exists, render the inline content into it
+ if (output.contentDOM) {
+ const contentFragment = serializer.serializeFragment(
+ node.content,
+ options,
+ );
+ output.contentDOM.dataset.editable = "";
+ output.contentDOM.appendChild(contentFragment);
+ }
+ continue;
+ }
+ }
+ } else if (node.type.name === "text") {
+ // We serialize text nodes manually as we need to serialize the styles/
+ // marks using `styleSpec.implementation.render`. When left up to
+ // ProseMirror, it'll use `toDOM` which is incorrect.
+ let dom: globalThis.Node | Text = document.createTextNode(
+ node.textContent,
+ );
+ // Reverse the order of marks to maintain the correct priority.
+ for (const mark of node.marks.toReversed()) {
+ if (mark.type.name in editor.schema.styleSpecs) {
+ const newDom = editor.schema.styleSpecs[
+ mark.type.name
+ ].implementation.render(mark.attrs["stringValue"], editor);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ } else {
+ const domOutputSpec = mark.type.spec.toDOM!(mark, true);
+ const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
+ newDom.contentDOM!.appendChild(dom);
+ dom = newDom.dom;
+ }
+ }
+
+ fragment.appendChild(dom);
+ } else {
+ // Fall back to default serialization for this node
+ const nodeFragment = serializer.serializeFragment(
+ Fragment.from([node]),
+ options,
+ );
+ fragment.appendChild(nodeFragment);
+ }
+ }
+
+ return fragment;
+}
+
+function serializeBlock<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ block: PartialBlock,
+ serializer: DOMSerializer,
+ options?: { document?: Document },
+) {
+ const BC_NODE = editor.pmSchema.nodes["blockContainer"];
+
+ // set default props in case we were passed a partial block
+ const props = block.props || {};
+ for (const [name, spec] of Object.entries(
+ editor.schema.blockSchema[block.type as any].propSchema,
+ )) {
+ if (!(name in props) && spec.default !== undefined) {
+ (props as any)[name] = spec.default;
+ }
+ }
+ const children = block.children || [];
+
+ const impl = editor.blockImplementations[block.type as any].implementation;
+ const ret = impl.render.call(
+ {
+ renderType: "dom",
+ props: undefined,
+ },
+ { ...block, props, children } as any,
+ editor as any,
+ );
+
+ if (ret.contentDOM && block.content) {
+ const ic = serializeInlineContentInternalHTML(
+ editor,
+ block.content as any, // TODO
+ serializer,
+ block.type,
+ options,
+ );
+ ret.contentDOM.appendChild(ic);
+ }
+
+ const pmType = editor.pmSchema.nodes[block.type as any];
+
+ if (pmType.isInGroup("bnBlock")) {
+ if (block.children && block.children.length > 0) {
+ const fragment = serializeBlocks(
+ editor,
+ block.children,
+ serializer,
+ options,
+ );
+
+ ret.contentDOM?.append(fragment);
+ }
+ return ret.dom;
+ }
+
+ // wrap the block in a blockContainer
+ const bc = BC_NODE.spec?.toDOM?.(
+ BC_NODE.create({
+ id: block.id,
+ ...props,
+ }),
+ ) as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+
+ bc.contentDOM?.appendChild(ret.dom);
+
+ if (block.children && block.children.length > 0) {
+ bc.contentDOM?.appendChild(
+ serializeBlocksInternalHTML(editor, block.children, serializer, options),
+ );
+ }
+ return bc.dom;
+}
+
+function serializeBlocks<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
+ serializer: DOMSerializer,
+ options?: { document?: Document },
+) {
+ const doc = options?.document ?? document;
+ const fragment = doc.createDocumentFragment();
+
+ for (const block of blocks) {
+ const blockDOM = serializeBlock(editor, block, serializer, options);
+ fragment.appendChild(blockDOM);
+ }
+
+ return fragment;
+}
+
+export const serializeBlocksInternalHTML = <
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema,
+>(
+ editor: BlockNoteEditor,
+ blocks: PartialBlock[],
+ serializer: DOMSerializer,
+ options?: { document?: Document },
+) => {
+ const BG_NODE = editor.pmSchema.nodes["blockGroup"];
+
+ const bg = BG_NODE.spec!.toDOM!(BG_NODE.create({})) as {
+ dom: HTMLElement;
+ contentDOM?: HTMLElement;
+ };
+
+ const fragment = serializeBlocks(editor, blocks, serializer, options);
+
+ bg.contentDOM?.appendChild(fragment);
+
+ return bg.dom;
+};
diff --git a/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
new file mode 100644
index 0000000000..7faa154dc6
--- /dev/null
+++ b/packages/core/src/api/exporters/markdown/htmlToMarkdown.ts
@@ -0,0 +1,813 @@
+/**
+ * Custom HTML-to-Markdown serializer for BlockNote.
+ * Replaces the unified/rehype-remark pipeline with a direct DOM-based implementation.
+ *
+ * Input: HTML string from createExternalHTMLExporter
+ * Output: GFM-compatible markdown string
+ */
+
+/**
+ * Convert an HTML string (from BlockNote's external HTML exporter) to markdown.
+ */
+export function htmlToMarkdown(html: string): string {
+ // Use a temporary element to parse HTML. This works in both browser and
+ // server (JSDOM) environments, unlike `new DOMParser()` which may not be
+ // globally available in Node.js.
+ const container = document.createElement("div");
+ container.innerHTML = html;
+ const result = serializeChildren(container, {
+ indent: "",
+ inListItem: false,
+ });
+ return result.trim() + "\n";
+}
+
+interface SerializeContext {
+ indent: string; // current indentation prefix for list nesting
+ // True when the current node is being serialized as continuation content
+ // of a parent list item. Used to suppress trailing blank lines that would
+ // otherwise turn the parent list into a "loose" list.
+ inListItem: boolean;
+}
+
+// ─── Main Serializer ─────────────────────────────────────────────────────────
+
+function serializeChildren(node: Node, ctx: SerializeContext): string {
+ let result = "";
+ const children = Array.from(node.childNodes);
+
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ result += serializeNode(child, ctx);
+ }
+
+ return result;
+}
+
+function serializeNode(node: Node, ctx: SerializeContext): string {
+ if (node.nodeType === 3 /* Node.TEXT_NODE */) {
+ return node.textContent || "";
+ }
+
+ if (node.nodeType !== 1 /* Node.ELEMENT_NODE */) {
+ return "";
+ }
+
+ const el = node as HTMLElement;
+ const tag = el.tagName.toLowerCase();
+
+ switch (tag) {
+ case "p":
+ return serializeParagraph(el, ctx);
+ case "h1":
+ case "h2":
+ case "h3":
+ case "h4":
+ case "h5":
+ case "h6":
+ return serializeHeading(el, ctx);
+ case "blockquote":
+ return serializeBlockquote(el, ctx);
+ case "pre":
+ return serializeCodeBlock(el, ctx);
+ case "ul":
+ return serializeUnorderedList(el, ctx);
+ case "ol":
+ return serializeOrderedList(el, ctx);
+ case "table":
+ return serializeTable(el, ctx);
+ case "hr":
+ return ctx.indent + "***\n\n";
+ case "img":
+ return serializeImage(el, ctx);
+ case "video":
+ return serializeVideo(el, ctx);
+ case "audio":
+ return serializeAudio(el, ctx);
+ case "embed":
+ return serializeEmbed(el, ctx);
+ case "figure":
+ return serializeFigure(el, ctx);
+ case "a":
+ // Block-level link (file block)
+ return serializeBlockLink(el, ctx);
+ case "details":
+ return serializeDetails(el, ctx);
+ case "div":
+ // Page break or generic container — serialize children
+ return serializeChildren(el, ctx);
+ case "br":
+ return "";
+ default:
+ return serializeChildren(el, ctx);
+ }
+}
+
+// ─── Block Serializers ───────────────────────────────────────────────────────
+
+function serializeParagraph(el: HTMLElement, ctx: SerializeContext): string {
+ const content = serializeInlineContent(el);
+ // Trim leading/trailing hard breaks (matching remark behavior)
+ const trimmed = trimHardBreaks(content);
+ if (ctx.inListItem) {
+ return trimmed;
+ }
+ return ctx.indent + trimmed + "\n\n";
+}
+
+function serializeHeading(el: HTMLElement, ctx: SerializeContext): string {
+ const level = parseInt(el.tagName[1], 10);
+ const prefix = "#".repeat(level) + " ";
+ const content = serializeInlineContent(el);
+ return ctx.indent + prefix + content + "\n\n";
+}
+
+function serializeBlockquote(el: HTMLElement, ctx: SerializeContext): string {
+ // Check if blockquote contains block-level elements (like
)
+ const blockChildren = Array.from(el.children).filter((child) => {
+ const tag = child.tagName.toLowerCase();
+ return ["p", "ul", "ol", "pre", "blockquote", "table", "hr"].includes(tag);
+ });
+
+ let content: string;
+ if (blockChildren.length > 0) {
+ // Has block-level children — serialize each
+ const parts: string[] = [];
+ for (const child of blockChildren) {
+ const tag = child.tagName.toLowerCase();
+ if (tag === "p") {
+ parts.push(serializeInlineContent(child as HTMLElement));
+ } else {
+ const innerCtx: SerializeContext = { indent: "", inListItem: false };
+ parts.push(serializeNode(child, innerCtx).trim());
+ }
+ }
+ content = parts.join("\n\n");
+ } else {
+ // No block-level children — treat entire content as inline
+ content = serializeInlineContent(el);
+ }
+
+ const lines = content.split("\n");
+ return lines.map((line) => ctx.indent + "> " + line).join("\n") + "\n\n";
+}
+
+function serializeCodeBlock(el: HTMLElement, ctx: SerializeContext): string {
+ const codeEl = el.querySelector("code");
+ if (!codeEl) {return "";}
+
+ const language =
+ codeEl.getAttribute("data-language") ||
+ extractLanguageFromClass(codeEl.className) ||
+ "";
+
+ // Extract code content, handling elements as newlines
+ const code = extractCodeContent(codeEl);
+
+ // Use a fence longer than the longest backtick run in the code
+ const longestRun = Math.max(
+ 0,
+ ...((code.match(/`+/g) ?? []).map((run) => run.length))
+ );
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
+
+ // For empty code blocks, don't add a newline between the fences
+ if (!code) {
+ return ctx.indent + fence + language + "\n" + fence + "\n\n";
+ }
+
+ return (
+ ctx.indent +
+ fence +
+ language +
+ "\n" +
+ code +
+ (code.endsWith("\n") ? "" : "\n") +
+ fence +
+ "\n\n"
+ );
+}
+
+function extractCodeContent(el: Element): string {
+ let result = "";
+ for (const child of Array.from(el.childNodes)) {
+ if (child.nodeType === 3 /* Node.TEXT_NODE */) {
+ result += child.textContent || "";
+ } else if (child.nodeType === 1 /* Node.ELEMENT_NODE */) {
+ const tag = (child as HTMLElement).tagName.toLowerCase();
+ if (tag === "br") {
+ result += "\n";
+ } else {
+ result += extractCodeContent(child as Element);
+ }
+ }
+ }
+ return result;
+}
+
+function extractLanguageFromClass(className: string): string {
+ const match = className.match(/language-(\S+)/);
+ return match ? match[1] : "";
+}
+
+function serializeUnorderedList(
+ el: HTMLElement,
+ ctx: SerializeContext
+): string {
+ let result = "";
+ const items = Array.from(el.children).filter(
+ (child) => child.tagName.toLowerCase() === "li"
+ );
+
+ for (const item of items) {
+ result += serializeListItem(item as HTMLElement, "bullet", ctx);
+ }
+
+ // Trailing blank line separates the list from the next block. Skip when
+ // this list is nested inside another list item — adding it would convert
+ // the parent list into a "loose" list (or break tightness).
+ if (!ctx.inListItem) {
+ result += "\n";
+ }
+ return result;
+}
+
+function serializeOrderedList(el: HTMLElement, ctx: SerializeContext): string {
+ let result = "";
+ const items = Array.from(el.children).filter(
+ (child) => child.tagName.toLowerCase() === "li"
+ );
+ const startNum = parseInt(el.getAttribute("start") || "1", 10);
+
+ for (let i = 0; i < items.length; i++) {
+ const num = startNum + i;
+ result += serializeListItem(items[i] as HTMLElement, "ordered", ctx, num);
+ }
+
+ if (!ctx.inListItem) {
+ result += "\n";
+ }
+ return result;
+}
+
+function serializeListItem(
+ el: HTMLElement,
+ listType: "bullet" | "ordered",
+ ctx: SerializeContext,
+ num?: number
+): string {
+ // Check for checkbox (task list) - direct children only
+ let checkbox: HTMLInputElement | null = null;
+ let details: HTMLElement | null = null;
+
+ for (const child of Array.from(el.children)) {
+ const tag = child.tagName.toLowerCase();
+ if (tag === "input" && (child as HTMLInputElement).type === "checkbox") {
+ checkbox = child as HTMLInputElement;
+ }
+ if (tag === "details") {
+ details = child as HTMLElement;
+ }
+ }
+
+ let marker: string;
+ let markerWidth: number;
+
+ if (checkbox) {
+ const state = checkbox.checked ? "[x]" : "[ ]";
+ marker = `* ${state} `;
+ // For child indentation, use bullet width (2), not full checkbox marker width
+ markerWidth = 2;
+ } else if (listType === "ordered") {
+ marker = `${num}. `;
+ markerWidth = marker.length;
+ } else {
+ marker = "* ";
+ markerWidth = 2;
+ }
+
+ // Collect the item's inline content
+ let inlineContent: string;
+ let firstContentEl: Element | null;
+
+ if (details) {
+ // Toggle item: get content from summary
+ const summary = details.querySelector("summary");
+ const summaryP = summary?.querySelector("p");
+ firstContentEl = details;
+ inlineContent = summaryP ? serializeInlineContent(summaryP) : "";
+ } else {
+ firstContentEl = getFirstContentElement(el, checkbox);
+ inlineContent = firstContentEl ? serializeInlineContent(firstContentEl) : "";
+ }
+
+ // The marker line ends with a single `\n` so that consecutive list items
+ // produce a "tight" list (no blank line between markers). Continuation
+ // content within the item (nested lists, continuation paragraphs, other
+ // blocks) injects its own spacing as needed.
+ let result = ctx.indent + marker + inlineContent + "\n";
+
+ // Serialize child content (nested lists, continuation paragraphs, etc.)
+ const childIndent = ctx.indent + " ".repeat(markerWidth);
+ const childCtx: SerializeContext = { indent: childIndent, inListItem: true };
+
+ // For toggle items, also serialize children inside the details element
+ if (details) {
+ const summary = details.querySelector("summary");
+ for (const child of Array.from(details.children)) {
+ if (child === summary) {continue;}
+ const childTag = child.tagName.toLowerCase();
+ if (childTag === "p") {
+ const content = serializeInlineContent(child as HTMLElement);
+ // Continuation paragraph needs a blank line to separate it from the
+ // previous content; CommonMark would otherwise treat it as a soft
+ // wrap of that content.
+ result += "\n" + childIndent + content + "\n";
+ } else {
+ result += serializeNode(child, childCtx);
+ }
+ }
+ }
+
+ const children = Array.from(el.children);
+ for (const child of children) {
+ const childTag = child.tagName.toLowerCase();
+
+ // Skip the first content element and checkbox
+ if (child === firstContentEl || (child as HTMLElement) === checkbox) {continue;}
+ if (childTag === "input") {continue;}
+
+ // Nested lists and other block content
+ if (childTag === "ul" || childTag === "ol") {
+ // Nested list flows directly under the parent marker — no blank line.
+ result += serializeNode(child, childCtx);
+ } else if (childTag === "p") {
+ // Continuation paragraph within list item — requires blank line before
+ // so it isn't read as part of the marker line's text.
+ const content = serializeInlineContent(child as HTMLElement);
+ result += "\n" + childIndent + content + "\n";
+ } else {
+ // Other block-level children (code blocks, blockquotes, etc.) already
+ // emit their own separating newlines; prefix with a blank line so they
+ // are recognized as separate blocks.
+ result += "\n" + serializeNode(child, childCtx);
+ }
+ }
+
+ return result;
+}
+
+function getFirstContentElement(
+ li: HTMLElement,
+ checkbox: HTMLInputElement | null
+): HTMLElement | null {
+ for (const child of Array.from(li.children)) {
+ if (child === checkbox) {continue;}
+ if (child.tagName.toLowerCase() === "input") {continue;}
+ const tag = child.tagName.toLowerCase();
+ if (tag === "p" || tag === "span") {return child as HTMLElement;}
+ }
+ return null;
+}
+
+// ─── Table Serializer ────────────────────────────────────────────────────────
+
+function serializeTable(el: HTMLElement, ctx: SerializeContext): string {
+ // First, determine column count from colgroup or first row
+ const colgroup = el.querySelector("colgroup");
+ let colCount = 0;
+
+ if (colgroup) {
+ colCount = colgroup.querySelectorAll("col").length;
+ }
+
+ const rows: string[][] = [];
+ let hasHeader = false;
+
+ // Collect all rows, handling colspan/rowspan
+ const trElements = el.querySelectorAll("tr");
+ // Build a grid to handle colspan/rowspan
+ const grid: (string | null)[][] = [];
+
+ trElements.forEach((tr, rowIdx) => {
+ if (!grid[rowIdx]) {grid[rowIdx] = [];}
+ const cellElements = tr.querySelectorAll("th, td");
+ let gridCol = 0;
+
+ cellElements.forEach((cell) => {
+ // Find next empty column in this row
+ while (grid[rowIdx][gridCol] !== undefined) {gridCol++;}
+
+ if (rowIdx === 0 && cell.tagName.toLowerCase() === "th") {
+ hasHeader = true;
+ }
+
+ const content = escapeTableCell(
+ serializeInlineContent(cell as HTMLElement).trim()
+ );
+ const colspan = parseInt(cell.getAttribute("colspan") || "1", 10);
+ const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10);
+
+ // Fill the grid
+ for (let r = 0; r < rowspan; r++) {
+ for (let c = 0; c < colspan; c++) {
+ const ri = rowIdx + r;
+ if (!grid[ri]) {grid[ri] = [];}
+ grid[ri][gridCol + c] = r === 0 && c === 0 ? content : "";
+ }
+ }
+
+ gridCol += colspan;
+ });
+
+ // Update colCount
+ if (grid[rowIdx]) {
+ colCount = Math.max(colCount, grid[rowIdx].length);
+ }
+ });
+
+ // Convert grid to rows
+ for (const gridRow of grid) {
+ const row: string[] = [];
+ for (let c = 0; c < colCount; c++) {
+ row.push(gridRow && gridRow[c] !== undefined ? (gridRow[c] ?? "") : "");
+ }
+ rows.push(row);
+ }
+
+ if (rows.length === 0) {return "";}
+
+ // Determine column widths
+ const colWidths: number[] = [];
+ for (let c = 0; c < colCount; c++) {
+ let maxWidth = 3; // minimum width for separator "---"
+ for (const row of rows) {
+ const cellWidth = c < row.length ? row[c].length : 0;
+ maxWidth = Math.max(maxWidth, cellWidth);
+ }
+ // Use minimum of 10 to match remark output
+ colWidths.push(Math.max(maxWidth, 10));
+ }
+
+ let result = "";
+
+ if (hasHeader) {
+ result += ctx.indent + formatTableRow(rows[0], colWidths, colCount) + "\n";
+ result += ctx.indent + formatSeparatorRow(colWidths, colCount) + "\n";
+ for (let r = 1; r < rows.length; r++) {
+ result +=
+ ctx.indent + formatTableRow(rows[r], colWidths, colCount) + "\n";
+ }
+ } else {
+ // No header — emit empty header + separator
+ const emptyRow = new Array(colCount).fill("");
+ result += ctx.indent + formatTableRow(emptyRow, colWidths, colCount) + "\n";
+ result += ctx.indent + formatSeparatorRow(colWidths, colCount) + "\n";
+ for (const row of rows) {
+ result +=
+ ctx.indent + formatTableRow(row, colWidths, colCount) + "\n";
+ }
+ }
+
+ result += "\n";
+ return result;
+}
+
+function escapeTableCell(text: string): string {
+ return text.replace(/\|/g, "\\|");
+}
+
+function formatTableRow(
+ cells: string[],
+ colWidths: number[],
+ colCount: number
+): string {
+ const parts: string[] = [];
+ for (let c = 0; c < colCount; c++) {
+ const cell = c < cells.length ? cells[c] : "";
+ parts.push(" " + cell.padEnd(colWidths[c]) + " ");
+ }
+ return "|" + parts.join("|") + "|";
+}
+
+function formatSeparatorRow(colWidths: number[], colCount: number): string {
+ const parts: string[] = [];
+ for (let c = 0; c < colCount; c++) {
+ parts.push(" " + "-".repeat(colWidths[c]) + " ");
+ }
+ return "|" + parts.join("|") + "|";
+}
+
+// ─── Media Serializers ───────────────────────────────────────────────────────
+
+function serializeImage(el: HTMLElement, ctx: SerializeContext): string {
+ const src = el.getAttribute("src") || "";
+ const alt = el.getAttribute("alt") || "";
+ // Empty placeholder — preserve the block-level break, matching how
+ // serializeParagraph/serializeHeading emit `\n\n` for empty content.
+ if (!src) {return "\n\n";}
+ return ctx.indent + `\n\n`;
+}
+
+function serializeVideo(el: HTMLElement, ctx: SerializeContext): string {
+ const src =
+ el.getAttribute("src") || el.getAttribute("data-url") || "";
+ const name = el.getAttribute("data-name") || el.getAttribute("title") || "";
+ if (!src) {return "\n\n";}
+ return ctx.indent + `\n\n`;
+}
+
+function serializeAudio(el: HTMLElement, ctx: SerializeContext): string {
+ const src = el.getAttribute("src") || "";
+ if (!src) {return "\n\n";}
+ // Audio has no markdown syntax, so emit raw HTML. The markdown parser
+ // passes