Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"playground": true,
"docs": true,
"author": "yousefed",
"tags": [
"Intermediate",
"Blocks",
"Custom Schemas",
"Combinator Content"
],
"dependencies": {
"@mantine/core": "^8.3.11",
"react-icons": "^5.5.0"
}
}
41 changes: 41 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Multi-Slot Alert Block

In this example, we create a custom `Alert` block whose content is a
**combinator content schema** — a record of two inline regions, `title` and
`body`. The block JSON exposes both slots as named keys, and the editor
displays the document's JSON live so you can see the resulting shape.

This is the same alert idea as `01-alert-block`, but with a richer content
shape: where the simple alert has a single inline region, this one has two
independently editable regions stored as named slots in the JSON.

```ts
const alertContentType = combinatorContentType(
"alert",
c.record({
title: c.inline(),
body: c.inline(),
}),
);
```

The block's content JSON is automatically derived from the schema:

```json
{
"type": "alert",
"props": { "variant": "warning" },
"content": {
"title": [{ "type": "text", "text": "Heads up", "styles": {} }],
"body": [{ "type": "text", "text": "Be careful.", "styles": {} }]
}
}
```

**Try it out:** click the icon to change the alert variant, and edit the title
and body inline. Watch the JSON panel below update in real time.

**Relevant Docs:**

- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)
- [Editor Setup](/docs/getting-started/editor-setup)
14 changes: 14 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multi-Slot Alert Block</title>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/main.tsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);
32 changes: 32 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@blocknote/example-custom-schema-multi-slot-alert-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": "^8.3.11",
"@mantine/hooks": "^8.3.11",
"@mantine/utils": "^6.0.22",
"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"
}
}
97 changes: 97 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/src/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { c, combinatorContentType } 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 variants of alert that users can choose from.
export const alertVariants = [
{ value: "warning", title: "Warning", icon: MdError },
{ value: "error", title: "Error", icon: MdCancel },
{ value: "info", title: "Info", icon: MdInfo },
{ value: "success", title: "Success", icon: MdCheckCircle },
] as const;

// The content schema: a record of two named inline regions. The block's
// `content` JSON shape is automatically derived as
// `{ title: InlineContent[]; body: InlineContent[] }`.
const alertContentType = combinatorContentType(
"alert",
c.record({
title: c.inline(),
body: c.inline(),
}),
);

export const createAlert = createReactBlockSpec(
{
type: "alert",
propSchema: {
variant: {
default: "warning",
values: ["warning", "error", "info", "success"] as const,
},
},
content: alertContentType,
},
{
render: (props) => {
const variant =
alertVariants.find((v) => v.value === props.block.props.variant) ??
alertVariants[0];
const Icon = variant.icon;

return (
<div
className="alert"
data-alert-variant={props.block.props.variant}>
{/* Icon — non-editable; opens a menu to change the variant. */}
<Menu withinPortal={false}>
<Menu.Target>
<div className="alert-icon-wrapper" contentEditable={false}>
<Icon
className="alert-icon"
data-alert-icon-variant={props.block.props.variant}
size={28}
/>
</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Alert variant</Menu.Label>
<Menu.Divider />
{alertVariants.map((v) => {
const ItemIcon = v.icon;
return (
<Menu.Item
key={v.value}
leftSection={
<ItemIcon
className="alert-icon"
data-alert-icon-variant={v.value}
/>
}
onClick={() =>
props.editor.updateBlock(props.block, {
type: "alert",
props: { variant: v.value },
})
}>
{v.title}
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
{/*
Content slots: the parent record's children (title + body) mount
as siblings inside this element. Each slot is a real ProseMirror
node, identified by `data-content-name="alert__<field>"`, which we
target with CSS to give title and body distinct styling.
*/}
<div className="alert-slots" ref={props.contentRef} />
</div>
);
},
},
);
75 changes: 75 additions & 0 deletions examples/06-custom-schema/09-multi-slot-alert-block/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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";
import { useEffect, useState } from "react";

import { createAlert } from "./Alert.js";
import "./styles.css";

// Schema with the multi-slot alert block added.
const schema = BlockNoteSchema.create({
blockSpecs: {
...defaultBlockSpecs,
alert: createAlert(),
},
});

export default function App() {
// The editor's `document` carries the full custom-schema type (including
// the `alert` block with its `{ title, body }` content), so we infer the
// state type from it instead of using the unparameterized `Block`.
const [blocks, setBlocks] = useState<typeof editor.document>([]);

// Editor preloaded with an example alert that has both slots populated, so
// the JSON panel below shows the `{ title, body }` shape from the start.
const editor = useCreateBlockNote({
schema,
initialContent: [
{
type: "paragraph",
content: "An alert below has two independent rich-text regions:",
},
{
type: "alert" as const,
props: { variant: "info" },
content: {
title: [
{ type: "text", text: "Heads up", styles: { bold: true } },
],
body: [
{ type: "text", text: "Title and body are ", styles: {} },
{ type: "text", text: "separate slots", styles: { italic: true } },
{ type: "text", text: " in the JSON.", styles: {} },
],
} as any,
} as any,
{
type: "paragraph",
content:
"Edit either slot and watch the JSON below — title and body update independently.",
},
],
});

useEffect(() => setBlocks(editor.document), [editor]);

return (
<div className="wrapper">
<div>BlockNote Editor:</div>
<div className="item">
<BlockNoteView
editor={editor}
onChange={() => setBlocks(editor.document)}
/>
</div>
<div>Document JSON:</div>
<div className="item bordered">
<pre>
<code>{JSON.stringify(blocks, null, 2)}</code>
</pre>
</div>
</div>
);
}
Loading
Loading