diff --git a/docs/pages/docs/advanced/grid-suggestion-menus.mdx b/docs/pages/docs/advanced/grid-suggestion-menus.mdx
new file mode 100644
index 0000000000..75d67b2c19
--- /dev/null
+++ b/docs/pages/docs/advanced/grid-suggestion-menus.mdx
@@ -0,0 +1,51 @@
+---
+title: Grid Suggestion Menus
+description: In addition to displaying Suggestion Menus as stacks, BlockNote also supports displaying them as grids.
+imageTitle: Grid Suggestion Menus
+---
+
+import { Example } from "@/components/example";
+
+# 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/advanced/grid-suggestion-menus#emoji-picker). For more information about how the mentions elements work, see [Custom Inline Content](/docs/custom-schemas/custom-inline-content).
diff --git a/docs/pages/docs/editor-basics/setup.mdx b/docs/pages/docs/editor-basics/setup.mdx
index e16960f469..e172f5b6cc 100644
--- a/docs/pages/docs/editor-basics/setup.mdx
+++ b/docs/pages/docs/editor-basics/setup.mdx
@@ -96,6 +96,7 @@ export type BlockNoteViewProps = {
linkToolbar?: boolean;
sideMenu?: boolean;
slashMenu?: boolean;
+ emojiPicker?: boolean;
filePanel?: boolean;
tableHandles?: boolean;
children?:
@@ -120,6 +121,8 @@ export type BlockNoteViewProps = {
`slashMenu`: Whether the [Slash Menu](/docs/ui-components/suggestion-menus#slash-menu) should be enabled.
+`emojiPicker`: Whether the [Emoji Picker](/docs/ui-components/suggestion-menus#emoji-picker) should be enabled.
+
`filePanel`: Whether the File Toolbar should be enabled.
`tableHandles`: Whether the Table Handles should be enabled.
diff --git a/docs/pages/docs/ui-components/suggestion-menus.mdx b/docs/pages/docs/ui-components/suggestion-menus.mdx
index e6ed27d4d8..e37c9f4bde 100644
--- a/docs/pages/docs/ui-components/suggestion-menus.mdx
+++ b/docs/pages/docs/ui-components/suggestion-menus.mdx
@@ -31,9 +31,9 @@ Passing `slashMenu={false}` to `BlockNoteView` tells BlockNote not to show the d
`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`).
-### Replacing the Suggestion Menu Component
+### Replacing the Slash Menu Component
-You can replace the React component used for the Suggestion Menu with your own, as you can see in the demo below.
+You can replace the React component used for the Slash Menu with your own, as you can see in the demo below.
@@ -48,3 +48,24 @@ You can add additional Suggestion Menus to the editor, which can use any trigger
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/ui-components/suggestion-menus#slash-menu). For more information about how the mentions elements work, see [Custom Inline Content](/docs/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.
\ No newline at end of file
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/examples/03-ui-components/07-suggestion-menus-slash-menu-component/styles.css b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/styles.css
index 61a6e95323..31bc1cf256 100644
--- a/examples/03-ui-components/07-suggestion-menus-slash-menu-component/styles.css
+++ b/examples/03-ui-components/07-suggestion-menus-slash-menu-component/styles.css
@@ -9,6 +9,10 @@
display: flex;
flex-direction: column;
gap: 8px;
+ height: fit-content;
+ max-height: 100%;
+
+ overflow: auto;
padding: 8px;
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/.bnexample.json b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/.bnexample.json
new file mode 100644
index 0000000000..3dfb6cf780
--- /dev/null
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": [
+ "Intermediate",
+ "Blocks",
+ "UI Components",
+ "Suggestion Menus",
+ "Emoji Picker"
+ ]
+}
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/App.tsx b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/App.tsx
new file mode 100644
index 0000000000..10e0b7f43b
--- /dev/null
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/App.tsx
@@ -0,0 +1,42 @@
+import "@blocknote/core/fonts/inter.css";
+import {
+ GridSuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+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 Emoji Picker",
+ },
+ {
+ type: "paragraph",
+ content: "There are now 5 columns instead of 10",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/README.md b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/README.md
new file mode 100644
index 0000000000..36487f8070
--- /dev/null
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/README.md
@@ -0,0 +1,10 @@
+# Changing Emoji Picker Columns
+
+In this example, we change the Emoji Picker to display 5 columns instead of 10.
+
+**Try it out:** Press the ":" key to open the Emoji Picker!
+
+**Relevant Docs:**
+
+- [Changing Emoji Picker Columns](/docs/ui-components/suggestion-menus#changing-emoji-picker-columns)
+- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/index.html b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/index.html
new file mode 100644
index 0000000000..83b0d80957
--- /dev/null
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Changing Emoji Picker Columns
+
+
+
+
+
+
diff --git a/examples/03-ui-components/08-uppy-file-panel/main.tsx b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/main.tsx
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/main.tsx
rename to examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/main.tsx
diff --git a/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json
new file mode 100644
index 0000000000..aa0685ea6c
--- /dev/null
+++ b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@blocknote/example-suggestion-menus-emoji-picker-columns",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/ariakit": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/shadcn": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^4.0.4",
+ "eslint": "^8.10.0",
+ "vite": "^4.4.8"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../../.eslintrc.js"
+ ]
+ },
+ "eslintIgnore": [
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/08-uppy-file-panel/tsconfig.json b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/tsconfig.json
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/tsconfig.json
rename to examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/tsconfig.json
diff --git a/examples/03-ui-components/08-uppy-file-panel/vite.config.ts b/examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/vite.config.ts
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/vite.config.ts
rename to examples/03-ui-components/08-suggestion-menus-emoji-picker-columns/vite.config.ts
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/.bnexample.json b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/.bnexample.json
new file mode 100644
index 0000000000..7bfc6d29e1
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": [
+ "Intermediate",
+ "UI Components",
+ "Suggestion Menus",
+ "Emoji Picker",
+ "Appearance & Styling"
+ ]
+}
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/App.tsx b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/App.tsx
new file mode 100644
index 0000000000..b895c0a5d6
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/App.tsx
@@ -0,0 +1,71 @@
+import "@blocknote/core/fonts/inter.css";
+import {
+ DefaultReactGridSuggestionItem,
+ GridSuggestionMenuController,
+ GridSuggestionMenuProps,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import "./styles.css";
+
+// Custom component to replace the default Emoji Picker.
+function CustomEmojiPicker(
+ props: GridSuggestionMenuProps
+) {
+ return (
+
+ {props.items.map((item, index) => (
+
{
+ props.onItemClick?.(item);
+ }}>
+ {item.icon}
+
+ ))}
+
+ );
+}
+
+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 Emoji Picker",
+ },
+ {
+ type: "paragraph",
+ content: "It's been replaced with a custom component",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+
+ );
+}
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/README.md b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/README.md
new file mode 100644
index 0000000000..f77d36e003
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/README.md
@@ -0,0 +1,10 @@
+# Replacing Emoji Picker Component
+
+In this example, we replace the default Emoji Picker component with a basic custom one.
+
+**Try it out:** Press the ":" key to see the new Emoji Picker!
+
+**Relevant Docs:**
+
+- [Replacing the Emoji Picker Component](/docs/ui-components/suggestion-menus#replacing-the-emoji-picker-component)
+- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/index.html b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/index.html
new file mode 100644
index 0000000000..4a239b4812
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Replacing Emoji Picker Component
+
+
+
+
+
+
diff --git a/examples/03-ui-components/09-custom-ui/main.tsx b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/main.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/main.tsx
rename to examples/03-ui-components/09-suggestion-menus-emoji-picker-component/main.tsx
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json
new file mode 100644
index 0000000000..aeebe0d7fd
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@blocknote/example-suggestion-menus-emoji-picker-component",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/ariakit": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/shadcn": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^4.0.4",
+ "eslint": "^8.10.0",
+ "vite": "^4.4.8"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../../.eslintrc.js"
+ ]
+ },
+ "eslintIgnore": [
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/styles.css b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/styles.css
new file mode 100644
index 0000000000..440e910d24
--- /dev/null
+++ b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/styles.css
@@ -0,0 +1,41 @@
+.emoji-picker {
+ z-index: 9999;
+
+ background-color: white;
+ border: 1px solid lightgray;
+ border-radius: 2px;
+ box-shadow: 0 0 8px #dddddd;
+
+ display: grid;
+ flex-direction: column;
+ gap: 8px;
+ height: fit-content;
+ max-height: 100%;
+
+ overflow: auto;
+
+ padding: 8px;
+
+ top: 8px;
+}
+
+.emoji-picker-item {
+ background-color: white;
+ border: 1px solid lightgray;
+ border-radius: 2px;
+ box-shadow: 0 0 4px #dddddd;
+
+ cursor: pointer;
+
+ font-size: 16px;
+
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+
+ padding: 8px;
+}
+
+.emoji-picker-item:hover, .emoji-picker-item.selected {
+ background-color: lightgray;
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/09-custom-ui/tsconfig.json b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/tsconfig.json
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/tsconfig.json
rename to examples/03-ui-components/09-suggestion-menus-emoji-picker-component/tsconfig.json
diff --git a/examples/03-ui-components/09-custom-ui/vite.config.ts b/examples/03-ui-components/09-suggestion-menus-emoji-picker-component/vite.config.ts
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/vite.config.ts
rename to examples/03-ui-components/09-suggestion-menus-emoji-picker-component/vite.config.ts
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/.bnexample.json b/examples/03-ui-components/10-suggestion-menus-grid-mentions/.bnexample.json
new file mode 100644
index 0000000000..f347671a56
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/.bnexample.json
@@ -0,0 +1,11 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "yousefed",
+ "tags": [
+ "Intermediate",
+ "Inline Content",
+ "Custom Schemas",
+ "Suggestion Menus"
+ ]
+}
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/App.tsx b/examples/03-ui-components/10-suggestion-menus-grid-mentions/App.tsx
new file mode 100644
index 0000000000..a8f0d748cb
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/App.tsx
@@ -0,0 +1,108 @@
+import {
+ BlockNoteSchema,
+ defaultInlineContentSpecs,
+ filterSuggestionItems,
+} from "@blocknote/core";
+import "@blocknote/core/fonts/inter.css";
+import {
+ DefaultReactGridSuggestionItem,
+ GridSuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+
+import { Mention } from "./Mention";
+
+// Our schema with inline content specs, which contain the configs and
+// implementations for inline content that we want our editor to use.
+const schema = BlockNoteSchema.create({
+ inlineContentSpecs: {
+ // Adds all default inline content.
+ ...defaultInlineContentSpecs,
+ // Adds the mention tag.
+ mention: Mention,
+ },
+});
+
+// Function which gets all users for the mentions menu.
+const getMentionMenuItems = (
+ editor: typeof schema.BlockNoteEditor
+): DefaultReactGridSuggestionItem[] => {
+ const users = ["Steve", "Bob", "Joe", "Mike"];
+
+ return users.map((user) => ({
+ id: user,
+ onItemClick: () => {
+ editor.insertInlineContent([
+ {
+ type: "mention",
+ props: {
+ user,
+ },
+ },
+ " ", // add a space after the mention
+ ]);
+ },
+ icon: {user.substring(0, 1)}
,
+ }));
+};
+
+export function App() {
+ const editor = useCreateBlockNote({
+ schema,
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "mention",
+ props: {
+ user: "Steve",
+ },
+ },
+ {
+ type: "text",
+ text: " <- This is an example mention",
+ styles: {},
+ },
+ ],
+ },
+ {
+ type: "paragraph",
+ content: "Press the '@' key to open the mentions menu and add another",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ return (
+
+ {/* Adds a mentions menu which opens with the "@" key */}
+
+ // Gets the mentions menu items
+ // TODO: Fix map/type cast
+ filterSuggestionItems(
+ getMentionMenuItems(editor).map((item) => ({
+ ...item,
+ title: item.id,
+ })),
+ query
+ ) as DefaultReactGridSuggestionItem[]
+ }
+ columns={2}
+ minQueryLength={2}
+ />
+
+ );
+}
+
+export default App;
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/Mention.tsx b/examples/03-ui-components/10-suggestion-menus-grid-mentions/Mention.tsx
new file mode 100644
index 0000000000..fdcae6b8c9
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/Mention.tsx
@@ -0,0 +1,21 @@
+import { createReactInlineContentSpec } from "@blocknote/react";
+
+// The Mention inline content.
+export const Mention = createReactInlineContentSpec(
+ {
+ type: "mention",
+ propSchema: {
+ user: {
+ default: "Unknown",
+ },
+ },
+ content: "none",
+ },
+ {
+ render: (props) => (
+
+ @{props.inlineContent.props.user}
+
+ ),
+ }
+);
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/README.md b/examples/03-ui-components/10-suggestion-menus-grid-mentions/README.md
new file mode 100644
index 0000000000..1696d491f5
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/README.md
@@ -0,0 +1,11 @@
+# Grid Mentions Menu
+
+In this example, we create a custom `Mention` inline content type which is used to tag people. In addition, we create a new Suggestion Menu for mentions which opens with the "@" character. This Suggestion Menu is displayed as a grid of 2 columns, where each item is the first letter of the person's name.
+
+**Try it out:** Press the "@" key to open the mentions menu and insert a mention!
+
+**Relevant Docs:**
+
+- [Custom Inline Content Types](/docs/custom-schemas/custom-inline-content)
+- [Creating Suggestion Menus](/docs/ui-components/suggestion-menus#creating-additional-suggestion-menus)
+- [Editor Setup](/docs/editor-basics/setup)
\ No newline at end of file
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/index.html b/examples/03-ui-components/10-suggestion-menus-grid-mentions/index.html
new file mode 100644
index 0000000000..98fa204a25
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Grid Mentions Menu
+
+
+
+
+
+
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/main.tsx b/examples/03-ui-components/10-suggestion-menus-grid-mentions/main.tsx
new file mode 100644
index 0000000000..f88b490fbd
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/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 "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json
new file mode 100644
index 0000000000..43bd3102d6
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@blocknote/example-suggestion-menus-grid-mentions",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --max-warnings 0"
+ },
+ "dependencies": {
+ "@blocknote/core": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/ariakit": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/shadcn": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^4.0.4",
+ "eslint": "^8.10.0",
+ "vite": "^4.4.8"
+ },
+ "eslintConfig": {
+ "extends": [
+ "../../../.eslintrc.js"
+ ]
+ },
+ "eslintIgnore": [
+ "dist"
+ ]
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/10-suggestion-menus-grid-mentions/tsconfig.json b/examples/03-ui-components/10-suggestion-menus-grid-mentions/tsconfig.json
new file mode 100644
index 0000000000..1bd8ab3c57
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/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": "Node",
+ "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/10-suggestion-menus-grid-mentions/vite.config.ts b/examples/03-ui-components/10-suggestion-menus-grid-mentions/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/10-suggestion-menus-grid-mentions/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/08-uppy-file-panel/.bnexample.json b/examples/03-ui-components/11-uppy-file-panel/.bnexample.json
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/.bnexample.json
rename to examples/03-ui-components/11-uppy-file-panel/.bnexample.json
diff --git a/examples/03-ui-components/08-uppy-file-panel/App.tsx b/examples/03-ui-components/11-uppy-file-panel/App.tsx
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/App.tsx
rename to examples/03-ui-components/11-uppy-file-panel/App.tsx
diff --git a/examples/03-ui-components/08-uppy-file-panel/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/FileReplaceButton.tsx
rename to examples/03-ui-components/11-uppy-file-panel/FileReplaceButton.tsx
diff --git a/examples/03-ui-components/08-uppy-file-panel/README.md b/examples/03-ui-components/11-uppy-file-panel/README.md
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/README.md
rename to examples/03-ui-components/11-uppy-file-panel/README.md
diff --git a/examples/03-ui-components/08-uppy-file-panel/UppyFilePanel.tsx b/examples/03-ui-components/11-uppy-file-panel/UppyFilePanel.tsx
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/UppyFilePanel.tsx
rename to examples/03-ui-components/11-uppy-file-panel/UppyFilePanel.tsx
diff --git a/examples/03-ui-components/08-uppy-file-panel/index.html b/examples/03-ui-components/11-uppy-file-panel/index.html
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/index.html
rename to examples/03-ui-components/11-uppy-file-panel/index.html
diff --git a/examples/03-ui-components/11-uppy-file-panel/main.tsx b/examples/03-ui-components/11-uppy-file-panel/main.tsx
new file mode 100644
index 0000000000..f88b490fbd
--- /dev/null
+++ b/examples/03-ui-components/11-uppy-file-panel/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 "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/08-uppy-file-panel/package.json b/examples/03-ui-components/11-uppy-file-panel/package.json
similarity index 100%
rename from examples/03-ui-components/08-uppy-file-panel/package.json
rename to examples/03-ui-components/11-uppy-file-panel/package.json
diff --git a/examples/03-ui-components/11-uppy-file-panel/tsconfig.json b/examples/03-ui-components/11-uppy-file-panel/tsconfig.json
new file mode 100644
index 0000000000..1bd8ab3c57
--- /dev/null
+++ b/examples/03-ui-components/11-uppy-file-panel/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": "Node",
+ "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/11-uppy-file-panel/vite.config.ts b/examples/03-ui-components/11-uppy-file-panel/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/11-uppy-file-panel/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/09-custom-ui/.bnexample.json b/examples/03-ui-components/12-custom-ui/.bnexample.json
similarity index 99%
rename from examples/03-ui-components/09-custom-ui/.bnexample.json
rename to examples/03-ui-components/12-custom-ui/.bnexample.json
index 9ded90803d..6c50d66ec7 100644
--- a/examples/03-ui-components/09-custom-ui/.bnexample.json
+++ b/examples/03-ui-components/12-custom-ui/.bnexample.json
@@ -7,4 +7,4 @@
"react-icons": "^5.2.1"
},
"pro": true
-}
+}
\ No newline at end of file
diff --git a/examples/03-ui-components/09-custom-ui/App.tsx b/examples/03-ui-components/12-custom-ui/App.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/App.tsx
rename to examples/03-ui-components/12-custom-ui/App.tsx
diff --git a/examples/03-ui-components/09-custom-ui/ColorMenu.tsx b/examples/03-ui-components/12-custom-ui/ColorMenu.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/ColorMenu.tsx
rename to examples/03-ui-components/12-custom-ui/ColorMenu.tsx
diff --git a/examples/03-ui-components/09-custom-ui/CustomFormattingToolbar.tsx b/examples/03-ui-components/12-custom-ui/CustomFormattingToolbar.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/CustomFormattingToolbar.tsx
rename to examples/03-ui-components/12-custom-ui/CustomFormattingToolbar.tsx
diff --git a/examples/03-ui-components/09-custom-ui/CustomSideMenu.tsx b/examples/03-ui-components/12-custom-ui/CustomSideMenu.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/CustomSideMenu.tsx
rename to examples/03-ui-components/12-custom-ui/CustomSideMenu.tsx
diff --git a/examples/03-ui-components/09-custom-ui/CustomSlashMenu.tsx b/examples/03-ui-components/12-custom-ui/CustomSlashMenu.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/CustomSlashMenu.tsx
rename to examples/03-ui-components/12-custom-ui/CustomSlashMenu.tsx
diff --git a/examples/03-ui-components/09-custom-ui/LinkMenu.tsx b/examples/03-ui-components/12-custom-ui/LinkMenu.tsx
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/LinkMenu.tsx
rename to examples/03-ui-components/12-custom-ui/LinkMenu.tsx
diff --git a/examples/03-ui-components/09-custom-ui/README.md b/examples/03-ui-components/12-custom-ui/README.md
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/README.md
rename to examples/03-ui-components/12-custom-ui/README.md
diff --git a/examples/03-ui-components/09-custom-ui/index.html b/examples/03-ui-components/12-custom-ui/index.html
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/index.html
rename to examples/03-ui-components/12-custom-ui/index.html
diff --git a/examples/03-ui-components/12-custom-ui/main.tsx b/examples/03-ui-components/12-custom-ui/main.tsx
new file mode 100644
index 0000000000..f88b490fbd
--- /dev/null
+++ b/examples/03-ui-components/12-custom-ui/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 "./App";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/03-ui-components/09-custom-ui/package.json b/examples/03-ui-components/12-custom-ui/package.json
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/package.json
rename to examples/03-ui-components/12-custom-ui/package.json
diff --git a/examples/03-ui-components/09-custom-ui/styles.css b/examples/03-ui-components/12-custom-ui/styles.css
similarity index 100%
rename from examples/03-ui-components/09-custom-ui/styles.css
rename to examples/03-ui-components/12-custom-ui/styles.css
diff --git a/examples/03-ui-components/12-custom-ui/tsconfig.json b/examples/03-ui-components/12-custom-ui/tsconfig.json
new file mode 100644
index 0000000000..1bd8ab3c57
--- /dev/null
+++ b/examples/03-ui-components/12-custom-ui/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": "Node",
+ "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/12-custom-ui/vite.config.ts b/examples/03-ui-components/12-custom-ui/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/12-custom-ui/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/package-lock.json b/package-lock.json
index 38d60815d5..8797c8db13 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3487,6 +3487,11 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@emoji-mart/data": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
+ "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="
+ },
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
@@ -9308,6 +9313,15 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/emoji-mart": {
+ "version": "3.0.14",
+ "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz",
+ "integrity": "sha512-/vMkVnet466bK37ugf2jry9ldCZklFPXYMB2m+qNo3vkP2I7L0cvtNFPKAjfcHgPg9Z8pbYqVqZn7AgsC0qf+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.56.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
@@ -13434,6 +13448,11 @@
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
},
+ "node_modules/emoji-mart": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
+ "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -29070,6 +29089,7 @@
"version": "0.14.5",
"license": "MPL-2.0",
"dependencies": {
+ "@emoji-mart/data": "^1.2.1",
"@tiptap/core": "^2.4.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-code": "^2.4.0",
@@ -29090,6 +29110,7 @@
"@tiptap/extension-text": "^2.4.0",
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/pm": "^2.4.0",
+ "emoji-mart": "^5.6.0",
"hast-util-from-dom": "^4.2.0",
"prosemirror-model": "^1.21.0",
"prosemirror-state": "^1.4.3",
@@ -29111,6 +29132,7 @@
"yjs": "^13.6.15"
},
"devDependencies": {
+ "@types/emoji-mart": "^3.0.14",
"@types/hast": "^2.3.4",
"@types/uuid": "^8.3.4",
"eslint": "^8.10.0",
diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx
index a4d7465562..e766e2c658 100644
--- a/packages/ariakit/src/index.tsx
+++ b/packages/ariakit/src/index.tsx
@@ -12,6 +12,10 @@ import {
import { ComponentProps } from "react";
import { Form } from "./input/Form";
+import { GridSuggestionMenu } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenu";
+import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem";
+import { GridSuggestionMenuItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem";
+import { GridSuggestionMenuLoader } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader";
import { TextInput } from "./input/TextInput";
import {
Menu,
@@ -54,6 +58,12 @@ export const components: Components = {
TabPanel: PanelTab,
TextInput: PanelTextInput,
},
+ GridSuggestionMenu: {
+ Root: GridSuggestionMenu,
+ Item: GridSuggestionMenuItem,
+ EmptyItem: GridSuggestionMenuEmptyItem,
+ Loader: GridSuggestionMenuLoader,
+ },
LinkToolbar: {
Root: Toolbar,
Button: ToolbarButton,
diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css
index ee3878107d..213f4d5cd9 100644
--- a/packages/ariakit/src/style.css
+++ b/packages/ariakit/src/style.css
@@ -6,128 +6,181 @@
@import "./ariakitStyles.css";
.bn-ak-input-wrapper {
- align-items: center;
- display: flex;
- gap: 0.5rem;
+ align-items: center;
+ display: flex;
+ gap: 0.5rem;
}
.bn-toolbar .bn-ak-button {
- width: unset;
+ width: unset;
}
.bn-toolbar .bn-ak-button[data-selected] {
- padding-top: 0.125rem;
- box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border);
+ padding-top: 0.125rem;
+ box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border);
}
.bn-toolbar .bn-ak-button[data-selected]:where(.dark, .dark *) {
- box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow);
+ box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow);
}
.bn-toolbar .bn-ak-popover {
- gap: 0.5rem;
+ gap: 0.5rem;
}
.bn-ariakit .bn-tab-panel {
- align-items: center;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
}
.bn-ariakit .bn-file-input {
- max-width: 100%;
+ max-width: 100%;
}
.bn-ak-button {
- outline-style: none;
+ outline-style: none;
}
-.bn-ak-menu-item[aria-selected="true"], .bn-ak-menu-item:hover {
- background-color: hsl(204 100% 40%);
- color: hsl(204 20% 100%);
+.bn-ak-menu-item[aria-selected="true"],
+.bn-ak-menu-item:hover {
+ background-color: hsl(204 100% 40%);
+ color: hsl(204 20% 100%);
}
.bn-ak-menu-item {
- display: flex;
+ display: flex;
}
.bn-ariakit .bn-dropdown {
- overflow: visible;
+ overflow: visible;
}
-.bn-ariakit .bn-suggestion-menu, .bn-ariakit .bn-color-picker-dropdown {
- overflow: scroll;
+.bn-ariakit .bn-suggestion-menu {
+ height: fit-content;
+ max-height: 100%;
+}
+
+.bn-ariakit .bn-color-picker-dropdown {
+ overflow: scroll;
}
.bn-ak-suggestion-menu-item-body {
- flex: 1;
+ flex: 1;
}
.bn-ak-suggestion-menu-item-subtitle {
- font-size: 0.7rem;
+ font-size: 0.7rem;
}
.bn-ak-suggestion-menu-item-section[data-position="left"] {
- padding: 8px;
+ padding: 8px;
}
.bn-ak-suggestion-menu-item-section[data-position="right"] {
- --border: rgb(0 0 0/13%);
- --highlight: rgb(255 255 255/20%);
- --shadow: rgb(0 0 0/10%);
- box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight),
+ --border: rgb(0 0 0/13%);
+ --highlight: rgb(255 255 255/20%);
+ --shadow: rgb(0 0 0/10%);
+ box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight),
inset 0 -1px 0 var(--shadow), 0 1px 1px var(--shadow);
- font-size: 0.7rem;
- border-radius: 4px;
- padding-inline: 4px;
+ font-size: 0.7rem;
+ border-radius: 4px;
+ padding-inline: 4px;
+}
+
+.bn-ariakit .bn-grid-suggestion-menu {
+ background: var(--bn-colors-menu-background);
+ border-radius: var(--bn-border-radius-large);
+ box-shadow: var(--bn-shadow-medium);
+ display: grid;
+ gap: 7px;
+ height: fit-content;
+ justify-items: center;
+ max-height: min(500px, 100%);
+ overflow-y: auto;
+ padding: 20px;
+}
+
+.bn-ariakit .bn-grid-suggestion-menu-item {
+ align-items: center;
+ border-radius: var(--bn-border-radius-large);
+ cursor: pointer;
+ display: flex;
+ font-size: 24px;
+ height: 32px;
+ justify-content: center;
+ margin: 2px;
+ padding: 4px;
+ width: 32px;
+}
+
+.bn-ariakit .bn-grid-suggestion-menu-item[aria-selected="true"],
+.bn-ariakit .bn-grid-suggestion-menu-item:hover {
+ background-color: var(--bn-colors-hovered-background);
+}
+
+.bn-ariakit .bn-grid-suggestion-menu-empty-item,
+.bn-ariakit .bn-grid-suggestion-menu-loader {
+ align-items: center;
+ color: var(--bn-colors-menu-text);
+ display: flex;
+ font-size: 14px;
+ font-weight: 500;
+ height: 32px;
+ justify-content: center;
+}
+
+.bn-ariakit .bn-grid-suggestion-menu-loader span {
+ background-color: var(--bn-colors-side-menu);
}
.bn-ariakit .bn-side-menu {
- align-items: center;
- display: flex;
- justify-content: center;
+ align-items: center;
+ display: flex;
+ justify-content: center;
}
.bn-side-menu .bn-ak-button {
- height: fit-content;
- padding: 0;
- width: fit-content;
+ height: fit-content;
+ padding: 0;
+ width: fit-content;
}
.bn-ariakit .bn-panel-popover {
- background-color: transparent;
- border: none;
- box-shadow: none;
+ background-color: transparent;
+ border: none;
+ box-shadow: none;
}
.bn-ariakit .bn-table-handle {
- height: fit-content;
- padding: 0;
- width: fit-content;
+ height: fit-content;
+ padding: 0;
+ width: fit-content;
}
.bn-ariakit .bn-side-menu,
.bn-ariakit .bn-table-handle {
- color: gray;
+ color: gray;
}
.bn-ak-button:where(.dark, .dark *) {
- color: hsl(204 20% 100%);
+ color: hsl(204 20% 100%);
}
-.bn-ak-tab, .bn-ariakit .bn-file-input {
- background-color: transparent;
- color: black;
+.bn-ak-tab,
+.bn-ariakit .bn-file-input {
+ background-color: transparent;
+ color: black;
}
.bn-ak-tab:where(.dark, .dark *),
.bn-ariakit .bn-file-input:where(.dark, .dark *) {
- color: white;
+ color: white;
}
.bn-ak-tooltip {
- align-items: center;
- display: flex;
- flex-direction: column;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
}
diff --git a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx
new file mode 100644
index 0000000000..b61454efe3
--- /dev/null
+++ b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx
@@ -0,0 +1,23 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenu = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Root"]
+>((props, ref) => {
+ const { className, children, id, columns, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx
new file mode 100644
index 0000000000..390cbb80e7
--- /dev/null
+++ b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx
@@ -0,0 +1,21 @@
+import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenuEmptyItem = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["EmptyItem"]
+>((props, ref) => {
+ const { className, children, columns, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ );
+});
diff --git a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
new file mode 100644
index 0000000000..6db77fda62
--- /dev/null
+++ b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
@@ -0,0 +1,40 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps, elementOverflow, mergeRefs } from "@blocknote/react";
+import { forwardRef, useEffect, useRef } from "react";
+
+export const GridSuggestionMenuItem = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Item"]
+>((props, ref) => {
+ const { className, isSelected, onClick, item, id, ...rest } = props;
+
+ assertEmpty(rest);
+
+ const itemRef = useRef(null);
+
+ useEffect(() => {
+ if (!itemRef.current || !isSelected) {
+ return;
+ }
+
+ const overflow = elementOverflow(itemRef.current);
+
+ if (overflow === "top") {
+ itemRef.current.scrollIntoView(true);
+ } else if (overflow === "bottom") {
+ itemRef.current.scrollIntoView(false);
+ }
+ }, [isSelected]);
+
+ return (
+
+ {item.icon}
+
+ );
+});
diff --git a/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx
new file mode 100644
index 0000000000..e78431e2ab
--- /dev/null
+++ b/packages/ariakit/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx
@@ -0,0 +1,26 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenuLoader = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Loader"]
+>((props, ref) => {
+ const {
+ className,
+ children, // unused, using "dots" instead
+ columns,
+ ...rest
+ } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/core/package.json b/packages/core/package.json
index f367cdbd4c..7378f4bdd2 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -54,6 +54,7 @@
"clean": "rimraf dist && rimraf types"
},
"dependencies": {
+ "@emoji-mart/data": "^1.2.1",
"@tiptap/core": "^2.4.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-code": "^2.4.0",
@@ -74,6 +75,7 @@
"@tiptap/extension-text": "^2.4.0",
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/pm": "^2.4.0",
+ "emoji-mart": "^5.6.0",
"hast-util-from-dom": "^4.2.0",
"prosemirror-model": "^1.21.0",
"prosemirror-state": "^1.4.3",
@@ -95,6 +97,7 @@
"yjs": "^13.6.15"
},
"devDependencies": {
+ "@types/emoji-mart": "^3.0.14",
"@types/hast": "^2.3.4",
"@types/uuid": "^8.3.4",
"eslint": "^8.10.0",
diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts
index 09c93cb89e..d16bddd67f 100644
--- a/packages/core/src/blocks/defaultBlockTypeGuards.ts
+++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts
@@ -6,7 +6,13 @@ import {
InlineContentSchema,
StyleSchema,
} from "../schema";
-import { Block, DefaultBlockSchema, defaultBlockSchema } from "./defaultBlocks";
+import {
+ Block,
+ DefaultBlockSchema,
+ defaultBlockSchema,
+ defaultInlineContentSchema,
+ DefaultInlineContentSchema,
+} from "./defaultBlocks";
import { defaultProps } from "./defaultProps";
export function checkDefaultBlockTypeInSchema<
@@ -23,6 +29,25 @@ export function checkDefaultBlockTypeInSchema<
);
}
+export function checkDefaultInlineContentTypeInSchema<
+ InlineContentType extends keyof DefaultInlineContentSchema,
+ B extends BlockSchema,
+ S extends StyleSchema
+>(
+ inlineContentType: InlineContentType,
+ editor: BlockNoteEditor
+): editor is BlockNoteEditor<
+ B,
+ { Type: DefaultInlineContentSchema[InlineContentType] },
+ S
+> {
+ return (
+ inlineContentType in editor.schema.inlineContentSchema &&
+ editor.schema.inlineContentSchema[inlineContentType] ===
+ defaultInlineContentSchema[inlineContentType]
+ );
+}
+
export function checkBlockIsDefaultType<
BlockType extends keyof DefaultBlockSchema,
I extends InlineContentSchema,
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index 8232db27d3..57e0a0be89 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -1038,4 +1038,16 @@ export class BlockNoteEditor<
this._tiptapEditor.off("selectionUpdate", cb);
};
}
+
+ public openSelectionMenu(triggerCharacter: string) {
+ this.prosemirrorView.focus();
+ this.prosemirrorView.dispatch(
+ this.prosemirrorView.state.tr
+ .scrollIntoView()
+ .setMeta(this.suggestionMenus.plugin, {
+ triggerCharacter: triggerCharacter,
+ fromUserInput: false,
+ })
+ );
+ }
}
diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
index 7ee7eacb9e..b1084cd773 100644
--- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
+++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts
@@ -12,7 +12,6 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
import { UiElementPosition } from "../../extensions-shared/UiElementPosition";
import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
import { EventEmitter } from "../../util/EventEmitter";
-import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin";
import { MultipleNodeSelection } from "./MultipleNodeSelection";
let dragImageElement: Element | undefined;
@@ -649,14 +648,8 @@ export class SideMenuView<
this.editor._tiptapEditor.commands.setTextSelection(startPos + 1);
}
- // Focuses and activates the suggestion menu.
- this.pmView.focus();
- this.pmView.dispatch(
- this.pmView.state.tr.scrollIntoView().setMeta(suggestionMenuPluginKey, {
- triggerCharacter: "/",
- fromUserInput: false,
- })
- );
+ // Focuses and activates the slash menu.
+ this.editor.openSelectionMenu("/");
}
}
diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts
new file mode 100644
index 0000000000..01d5aea707
--- /dev/null
+++ b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts
@@ -0,0 +1,4 @@
+export type DefaultGridSuggestionItem = {
+ id: string;
+ onItemClick: () => void;
+};
diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
index d64bd83984..7729108732 100644
--- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
+++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
@@ -139,7 +139,7 @@ type SuggestionPluginState =
}
| undefined;
-export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
+const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin");
/**
* A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions.
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts
new file mode 100644
index 0000000000..9f725f76e9
--- /dev/null
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts
@@ -0,0 +1,38 @@
+import data, { Emoji, EmojiMartData } from "@emoji-mart/data";
+import { init, SearchIndex } from "emoji-mart";
+
+import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema";
+import { BlockNoteEditor } from "../../editor/BlockNoteEditor";
+import { checkDefaultInlineContentTypeInSchema } from "../../blocks/defaultBlockTypeGuards";
+import { DefaultGridSuggestionItem } from "./DefaultGridSuggestionItem";
+
+let dataInitialized = false;
+const emojiMartData = data as EmojiMartData;
+
+export async function getDefaultEmojiPickerItems<
+ BSchema extends BlockSchema,
+ I extends InlineContentSchema,
+ S extends StyleSchema
+>(
+ editor: BlockNoteEditor,
+ query: string
+): Promise {
+ if (!checkDefaultInlineContentTypeInSchema("text", editor)) {
+ return [];
+ }
+
+ if (!dataInitialized) {
+ dataInitialized = true;
+ await init({ emojiMartData });
+ }
+
+ const emojisToShow =
+ query.trim() === ""
+ ? Object.values(emojiMartData.emojis)
+ : ((await SearchIndex.search(query)) as Emoji[]);
+
+ return emojisToShow.map((emoji: Emoji) => ({
+ id: emoji.skins[0].native,
+ onItemClick: () => editor.insertInlineContent(emoji.skins[0].native + " "),
+ }));
+}
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
index e482370c44..72994f5d04 100644
--- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
@@ -1,5 +1,6 @@
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor";
import { Block, PartialBlock } from "../../blocks/defaultBlocks";
+
import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards";
import {
BlockSchema,
@@ -270,6 +271,12 @@ export function getDefaultSlashMenuItems<
});
}
+ items.push({
+ onItemClick: () => editor.openSelectionMenu(":"),
+ key: "emoji",
+ ...editor.dictionary.slash_menu.emoji,
+ });
+
return items;
}
diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts
index 525d56b2a1..7c81fd8e73 100644
--- a/packages/core/src/i18n/locales/ar.ts
+++ b/packages/core/src/i18n/locales/ar.ts
@@ -90,6 +90,12 @@ export const ar: Dictionary = {
aliases: ["ملف", "تحميل", "تضمين", "وسائط", "رابط"],
group: "وسائط",
},
+ emoji: {
+ title: "الرموز التعبيرية",
+ subtext: "تُستخدم لإدراج رمز تعبيري",
+ aliases: ["رمز تعبيري", "إيموجي", "إيموت", "عاطفة", "وجه"],
+ group: "آخرون",
+ },
},
placeholders: {
default: "أدخل نصًا أو اكتب '/' للأوامر",
diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts
index 9edbb6df20..60ebf8a927 100644
--- a/packages/core/src/i18n/locales/en.ts
+++ b/packages/core/src/i18n/locales/en.ts
@@ -2,37 +2,37 @@ export const en = {
slash_menu: {
heading: {
title: "Heading 1",
- subtext: "Used for a top-level heading",
+ subtext: "Top-level heading",
aliases: ["h", "heading1", "h1"],
group: "Headings",
},
heading_2: {
title: "Heading 2",
- subtext: "Used for key sections",
+ subtext: "Key section heading",
aliases: ["h2", "heading2", "subheading"],
group: "Headings",
},
heading_3: {
title: "Heading 3",
- subtext: "Used for subsections and group headings",
+ subtext: "Subsection and group heading",
aliases: ["h3", "heading3", "subheading"],
group: "Headings",
},
numbered_list: {
title: "Numbered List",
- subtext: "Used to display a numbered list",
+ subtext: "List with ordered items",
aliases: ["ol", "li", "list", "numberedlist", "numbered list"],
group: "Basic blocks",
},
bullet_list: {
title: "Bullet List",
- subtext: "Used to display an unordered list",
+ subtext: "List with unordered items",
aliases: ["ul", "li", "list", "bulletlist", "bullet list"],
group: "Basic blocks",
},
check_list: {
title: "Check List",
- subtext: "Used to display a list with checkboxes",
+ subtext: "List with checkboxes",
aliases: [
"ul",
"li",
@@ -46,19 +46,19 @@ export const en = {
},
paragraph: {
title: "Paragraph",
- subtext: "Used for the body of your document",
+ subtext: "The body of your document",
aliases: ["p", "paragraph"],
group: "Basic blocks",
},
table: {
title: "Table",
- subtext: "Used for tables",
+ subtext: "Table with editable cells",
aliases: ["table"],
group: "Advanced",
},
image: {
title: "Image",
- subtext: "Insert an image",
+ subtext: "Resizable image with caption",
aliases: [
"image",
"imageUpload",
@@ -72,7 +72,7 @@ export const en = {
},
video: {
title: "Video",
- subtext: "Insert a video",
+ subtext: "Resizable video with caption",
aliases: [
"video",
"videoUpload",
@@ -86,7 +86,7 @@ export const en = {
},
audio: {
title: "Audio",
- subtext: "Insert audio",
+ subtext: "Embedded audio with caption",
aliases: [
"audio",
"audioUpload",
@@ -100,10 +100,16 @@ export const en = {
},
file: {
title: "File",
- subtext: "Insert a file",
+ subtext: "Embedded file",
aliases: ["file", "upload", "embed", "media", "url"],
group: "Media",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Search for and insert an emoji",
+ aliases: ["emoji", "emote", "emotion", "face"],
+ group: "Others",
+ },
},
placeholders: {
default: "Enter text or type '/' for commands",
diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts
index c4345c6631..bd9e073aa0 100644
--- a/packages/core/src/i18n/locales/fr.ts
+++ b/packages/core/src/i18n/locales/fr.ts
@@ -105,6 +105,12 @@ export const fr: Dictionary = {
aliases: ["fichier", "téléverser", "intégrer", "média", "url"],
group: "Média",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Utilisé pour insérer un emoji",
+ aliases: ["emoji", "émoticône", "émotion", "visage"],
+ group: "Autres",
+ },
},
placeholders: {
default: "Entrez du texte ou tapez '/' pour les commandes",
diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts
index 729dc91ddc..4b4ee544d7 100644
--- a/packages/core/src/i18n/locales/is.ts
+++ b/packages/core/src/i18n/locales/is.ts
@@ -98,6 +98,12 @@ export const is: Dictionary = {
aliases: ["skrá", "hlaða upp", "fella inn", "miðill", "url"],
group: "Miðlar",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Notað til að setja inn smámynd",
+ aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"],
+ group: "Annað",
+ },
},
placeholders: {
default: "Sláðu inn texta eða skrifaðu '/' fyrir skipanir",
diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts
index 59dbc267ad..91c7d983de 100644
--- a/packages/core/src/i18n/locales/ja.ts
+++ b/packages/core/src/i18n/locales/ja.ts
@@ -125,6 +125,12 @@ export const ja: Dictionary = {
aliases: ["file", "upload", "embed", "media", "url", "ファイル"],
group: "メディア",
},
+ emoji: {
+ title: "絵文字",
+ subtext: "絵文字を挿入するために使用します",
+ aliases: ["絵文字", "顔文字", "感情表現", "顔"],
+ group: "その他",
+ },
},
placeholders: {
default: "テキストを入力するか'/' を入力してコマンド選択",
diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts
index 20127fc976..9c7f6bf92a 100644
--- a/packages/core/src/i18n/locales/ko.ts
+++ b/packages/core/src/i18n/locales/ko.ts
@@ -109,6 +109,21 @@ export const ko: Dictionary = {
aliases: ["file", "upload", "embed", "media", "파일", "url"],
group: "미디어",
},
+ emoji: {
+ title: "이모지",
+ subtext: "이모지 삽입용으로 사용됩니다",
+ aliases: [
+ "이모지",
+ "emoji",
+ "감정 표현",
+ "emotion expression",
+ "표정",
+ "face expression",
+ "얼굴",
+ "face",
+ ],
+ group: "기타",
+ },
},
placeholders: {
default: "텍스트를 입력하거나 /를 입력하여 명령을 입력하세요.",
diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts
index 1cd8cf747a..782645c1a1 100644
--- a/packages/core/src/i18n/locales/nl.ts
+++ b/packages/core/src/i18n/locales/nl.ts
@@ -100,6 +100,17 @@ export const nl: Dictionary = {
aliases: ["bestand", "upload", "insluiten", "media", "url"],
group: "Media",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Gebruikt voor het invoegen van een emoji",
+ aliases: [
+ "emoji",
+ "emotie-uitdrukking",
+ "gezichtsuitdrukking",
+ "gezicht",
+ ],
+ group: "Overig",
+ },
},
placeholders: {
default: "Voer tekst in of type '/' voor commando's",
diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts
index e5647c1b31..89aedc0a54 100644
--- a/packages/core/src/i18n/locales/pl.ts
+++ b/packages/core/src/i18n/locales/pl.ts
@@ -90,6 +90,12 @@ export const pl: Dictionary = {
aliases: ["plik", "wrzuć", "wstaw", "media", "url"],
group: "Media",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Używane do wstawiania emoji",
+ aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"],
+ group: "Inne",
+ },
},
placeholders: {
default: "Wprowadź tekst lub wpisz '/' aby użyć poleceń",
diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts
index 946489553d..6ee76d1666 100644
--- a/packages/core/src/i18n/locales/pt.ts
+++ b/packages/core/src/i18n/locales/pt.ts
@@ -97,6 +97,12 @@ export const pt: Dictionary = {
aliases: ["arquivo", "upload", "incorporar", "mídia", "url"],
group: "Mídia",
},
+ emoji: {
+ title: "Emoji",
+ subtext: "Usado para inserir um emoji",
+ aliases: ["emoji", "emoticon", "expressão emocional", "rosto"],
+ group: "Outros",
+ },
},
placeholders: {
default: "Digite texto ou use '/' para comandos",
diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts
index d1ae8d9fb9..51a8f2653f 100644
--- a/packages/core/src/i18n/locales/ru.ts
+++ b/packages/core/src/i18n/locales/ru.ts
@@ -1,323 +1,336 @@
import { Dictionary } from "../dictionary";
- export const ru: Dictionary = {
- slash_menu: {
- heading: {
- title: "Заголовок 1 уровня",
- subtext: "Используется для заголовка верхнего уровня",
- aliases: ["h", "heading1", "h1", "заголовок1"],
- group: "Заголовки",
- },
- heading_2: {
- title: "Заголовок 2 уровня",
- subtext: "Используется для ключевых разделов",
- aliases: ["h2", "heading2", "subheading", "заголовок2", "подзаголовок"],
- group: "Заголовки",
- },
- heading_3: {
- title: "Заголовок 3 уровня",
- subtext: "Используется для подразделов и групп",
- aliases: ["h3", "heading3", "subheading", "заголовок3", "подзаголовок"],
- group: "Заголовки",
- },
- numbered_list: {
- title: "Нумерованный список",
- subtext: "Используется для отображения нумерованного списка",
- aliases: [
- "ol",
- "li",
- "list",
- "numberedlist",
- "numbered list",
- "список",
- "нумерованный список",
- ],
- group: "Базовые блоки",
- },
- bullet_list: {
- title: "Маркированный список",
- subtext: "Для отображения неупорядоченного списка.",
- aliases: ["ul", "li", "list", "bulletlist", "bullet list", "список", "маркированный список"],
- group: "Базовые блоки",
- },
- check_list: {
- title: "Контрольный список",
- subtext: "Для отображения списка с флажками",
- aliases: [
- "ul",
- "li",
- "list",
- "checklist",
- "check list",
- "checked list",
- "checkbox",
- "список",
- ],
- group: "Базовые блоки",
- },
- paragraph: {
- title: "Параграф",
- subtext: "Основной текст",
- aliases: ["p", "paragraph", "параграф"],
- group: "Базовые блоки",
- },
- table: {
- title: "Таблица",
- subtext: "Используется для таблиц",
- aliases: ["table", "таблица"],
- group: "Продвинутый",
- },
- image: {
- title: "Картинка",
- subtext: "Вставить изображение",
- aliases: [
- "image",
- "imageUpload",
- "upload",
- "img",
- "picture",
- "media",
- "url",
- "загрузка",
- "картинка",
- "рисунок",
- ],
- group: "Медиа",
- },
- video: {
- title: "Видео",
- subtext: "Вставить видео",
- aliases: [
- "video",
- "videoUpload",
- "upload",
- "mp4",
- "film",
- "media",
- "url",
- "загрузка",
- "видео",
- ],
- group: "Медиа",
- },
- audio: {
- title: "Аудио",
- subtext: "Вставить аудио",
- aliases: [
- "audio",
- "audioUpload",
- "upload",
- "mp3",
- "sound",
- "media",
- "url",
- "загрузка",
- "аудио",
- "звук",
- "музыка",
- ],
- group: "Медиа",
- },
- file: {
- title: "Файл",
- subtext: "Вставить файл",
- aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"],
- group: "Медиа",
- },
+export const ru: Dictionary = {
+ slash_menu: {
+ heading: {
+ title: "Заголовок 1 уровня",
+ subtext: "Используется для заголовка верхнего уровня",
+ aliases: ["h", "heading1", "h1", "заголовок1"],
+ group: "Заголовки",
},
- placeholders: {
- default: "Ведите текст или введите «/» для команд",
- heading: "Заголовок",
- bulletListItem: "Список",
- numberedListItem: "Список",
- checkListItem: "Список",
+ heading_2: {
+ title: "Заголовок 2 уровня",
+ subtext: "Используется для ключевых разделов",
+ aliases: ["h2", "heading2", "subheading", "заголовок2", "подзаголовок"],
+ group: "Заголовки",
},
- file_blocks: {
- image: {
- add_button_text: "Добавить изображение",
- },
- video: {
- add_button_text: "Добавить видео",
- },
- audio: {
- add_button_text: "Добавить аудио",
- },
- file: {
- add_button_text: "Добавить файл",
- },
+ heading_3: {
+ title: "Заголовок 3 уровня",
+ subtext: "Используется для подразделов и групп",
+ aliases: ["h3", "heading3", "subheading", "заголовок3", "подзаголовок"],
+ group: "Заголовки",
},
- // from react package:
- side_menu: {
- add_block_label: "Добавить блок",
- drag_handle_label: "Открыть меню блока",
- },
- drag_handle: {
- delete_menuitem: "Удалить",
- colors_menuitem: "Цвета",
- },
- table_handle: {
- delete_column_menuitem: "Удалить столбец",
- delete_row_menuitem: "Удалить строку",
- add_left_menuitem: "Добавить столбец слева",
- add_right_menuitem: "Добавить столбец справа",
- add_above_menuitem: "Добавить строку выше",
- add_below_menuitem: "Добавить строку ниже",
- },
- suggestion_menu: {
- no_items_title: "ничего не найдено",
- loading: "Загрузка…",
- },
- color_picker: {
- text_title: "Текст",
- background_title: "Задний фон",
- colors: {
- default: "По умолчинию",
- gray: "Серый",
- brown: "Коричневый",
- red: "Красный",
- orange: "Оранжевый",
- yellow: "Жёлтый",
- green: "Зелёный",
- blue: "Голубой",
- purple: "Фиолетовый",
- pink: "Розовый",
- },
+ numbered_list: {
+ title: "Нумерованный список",
+ subtext: "Используется для отображения нумерованного списка",
+ aliases: [
+ "ol",
+ "li",
+ "list",
+ "numberedlist",
+ "numbered list",
+ "список",
+ "нумерованный список",
+ ],
+ group: "Базовые блоки",
},
-
- formatting_toolbar: {
- bold: {
- tooltip: "Жирный",
- secondary_tooltip: "Mod+B",
- },
- italic: {
- tooltip: "Курсив",
- secondary_tooltip: "Mod+I",
- },
- underline: {
- tooltip: "Подчёркнутый",
- secondary_tooltip: "Mod+U",
- },
- strike: {
- tooltip: "Зачёркнутый",
- secondary_tooltip: "Mod+Shift+X",
- },
- code: {
- tooltip: "Код",
- secondary_tooltip: "",
- },
- colors: {
- tooltip: "Цвета",
- },
- link: {
- tooltip: "Создать ссылку",
- secondary_tooltip: "Mod+K",
- },
- file_caption: {
- tooltip: "Изменить подпись",
- input_placeholder: "Изменить подпись",
- },
- file_replace: {
- tooltip: {
- image: "Заменить изображение",
- video: "Заменить видео",
- audio: "Заменить аудио",
- file: "Заменить файл",
- },
- },
- file_rename: {
- tooltip: {
- image: "Переименовать изображение",
- video: "Переименовать видео",
- audio: "Переименовать аудио",
- file: "Переименовать файл",
- },
- input_placeholder: {
- image: "Переименовать изображение",
- video: "Переименовать видео",
- audio: "Переименовать аудио",
- file: "Переименовать файл",
- },
- },
- file_download: {
- tooltip: {
- image: "Скачать картинку",
- video: "Скачать видео",
- audio: "Скачать аудио",
- file: "Скачать файл",
- },
- },
- file_delete: {
- tooltip: {
- image: "Удалить картинку",
- video: "Удалить видео",
- audio: "Удалить аудио",
- file: "Скачать файл",
- },
- },
- file_preview_toggle: {
- tooltip: "Переключить предварительный просмотр",
- },
- nest: {
- tooltip: "Сдвинуть вправо",
- secondary_tooltip: "Tab",
- },
- unnest: {
- tooltip: "Сдвинуть влево",
- secondary_tooltip: "Shift+Tab",
- },
- align_left: {
- tooltip: "Текст по левому краю",
- },
- align_center: {
- tooltip: "Текст по середине",
- },
- align_right: {
- tooltip: "Текст по правому краю",
- },
- align_justify: {
- tooltip: "По середине текст",
- },
+ bullet_list: {
+ title: "Маркированный список",
+ subtext: "Для отображения неупорядоченного списка.",
+ aliases: [
+ "ul",
+ "li",
+ "list",
+ "bulletlist",
+ "bullet list",
+ "список",
+ "маркированный список",
+ ],
+ group: "Базовые блоки",
},
- file_panel: {
- upload: {
- title: "Загрузить",
- file_placeholder: {
- image: "Загрузить картинки",
- video: "Загрузить видео",
- audio: "Загрузить аудио",
- file: "Загрузить файл",
- },
- upload_error: "Ошибка: не удалось загрузить",
- },
- embed: {
- title: "Вставить",
- embed_button: {
- image: "Вставить картинку",
- video: "Вставить видео",
- audio: "Вставить аудио",
- file: "Вставить файл",
- },
- url_placeholder: "Введите URL",
- },
+ check_list: {
+ title: "Контрольный список",
+ subtext: "Для отображения списка с флажками",
+ aliases: [
+ "ul",
+ "li",
+ "list",
+ "checklist",
+ "check list",
+ "checked list",
+ "checkbox",
+ "список",
+ ],
+ group: "Базовые блоки",
+ },
+ paragraph: {
+ title: "Параграф",
+ subtext: "Основной текст",
+ aliases: ["p", "paragraph", "параграф"],
+ group: "Базовые блоки",
+ },
+ table: {
+ title: "Таблица",
+ subtext: "Используется для таблиц",
+ aliases: ["table", "таблица"],
+ group: "Продвинутый",
+ },
+ image: {
+ title: "Картинка",
+ subtext: "Вставить изображение",
+ aliases: [
+ "image",
+ "imageUpload",
+ "upload",
+ "img",
+ "picture",
+ "media",
+ "url",
+ "загрузка",
+ "картинка",
+ "рисунок",
+ ],
+ group: "Медиа",
+ },
+ video: {
+ title: "Видео",
+ subtext: "Вставить видео",
+ aliases: [
+ "video",
+ "videoUpload",
+ "upload",
+ "mp4",
+ "film",
+ "media",
+ "url",
+ "загрузка",
+ "видео",
+ ],
+ group: "Медиа",
+ },
+ audio: {
+ title: "Аудио",
+ subtext: "Вставить аудио",
+ aliases: [
+ "audio",
+ "audioUpload",
+ "upload",
+ "mp3",
+ "sound",
+ "media",
+ "url",
+ "загрузка",
+ "аудио",
+ "звук",
+ "музыка",
+ ],
+ group: "Медиа",
+ },
+ file: {
+ title: "Файл",
+ subtext: "Вставить файл",
+ aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"],
+ group: "Медиа",
+ },
+ emoji: {
+ title: "Эмодзи",
+ subtext: "Используется для вставки эмодзи",
+ aliases: ["эмодзи", "смайлик", "выражение эмоций", "лицо"],
+ group: "Прочее",
+ },
+ },
+ placeholders: {
+ default: "Ведите текст или введите «/» для команд",
+ heading: "Заголовок",
+ bulletListItem: "Список",
+ numberedListItem: "Список",
+ checkListItem: "Список",
+ },
+ file_blocks: {
+ image: {
+ add_button_text: "Добавить изображение",
+ },
+ video: {
+ add_button_text: "Добавить видео",
+ },
+ audio: {
+ add_button_text: "Добавить аудио",
+ },
+ file: {
+ add_button_text: "Добавить файл",
+ },
+ },
+ // from react package:
+ side_menu: {
+ add_block_label: "Добавить блок",
+ drag_handle_label: "Открыть меню блока",
+ },
+ drag_handle: {
+ delete_menuitem: "Удалить",
+ colors_menuitem: "Цвета",
+ },
+ table_handle: {
+ delete_column_menuitem: "Удалить столбец",
+ delete_row_menuitem: "Удалить строку",
+ add_left_menuitem: "Добавить столбец слева",
+ add_right_menuitem: "Добавить столбец справа",
+ add_above_menuitem: "Добавить строку выше",
+ add_below_menuitem: "Добавить строку ниже",
+ },
+ suggestion_menu: {
+ no_items_title: "ничего не найдено",
+ loading: "Загрузка…",
+ },
+ color_picker: {
+ text_title: "Текст",
+ background_title: "Задний фон",
+ colors: {
+ default: "По умолчинию",
+ gray: "Серый",
+ brown: "Коричневый",
+ red: "Красный",
+ orange: "Оранжевый",
+ yellow: "Жёлтый",
+ green: "Зелёный",
+ blue: "Голубой",
+ purple: "Фиолетовый",
+ pink: "Розовый",
+ },
+ },
+
+ formatting_toolbar: {
+ bold: {
+ tooltip: "Жирный",
+ secondary_tooltip: "Mod+B",
+ },
+ italic: {
+ tooltip: "Курсив",
+ secondary_tooltip: "Mod+I",
+ },
+ underline: {
+ tooltip: "Подчёркнутый",
+ secondary_tooltip: "Mod+U",
+ },
+ strike: {
+ tooltip: "Зачёркнутый",
+ secondary_tooltip: "Mod+Shift+X",
+ },
+ code: {
+ tooltip: "Код",
+ secondary_tooltip: "",
},
- link_toolbar: {
- delete: {
- tooltip: "Удалить ссылку",
+ colors: {
+ tooltip: "Цвета",
+ },
+ link: {
+ tooltip: "Создать ссылку",
+ secondary_tooltip: "Mod+K",
+ },
+ file_caption: {
+ tooltip: "Изменить подпись",
+ input_placeholder: "Изменить подпись",
+ },
+ file_replace: {
+ tooltip: {
+ image: "Заменить изображение",
+ video: "Заменить видео",
+ audio: "Заменить аудио",
+ file: "Заменить файл",
},
- edit: {
- text: "Изменить ссылку",
- tooltip: "Редактировать",
+ },
+ file_rename: {
+ tooltip: {
+ image: "Переименовать изображение",
+ video: "Переименовать видео",
+ audio: "Переименовать аудио",
+ file: "Переименовать файл",
+ },
+ input_placeholder: {
+ image: "Переименовать изображение",
+ video: "Переименовать видео",
+ audio: "Переименовать аудио",
+ file: "Переименовать файл",
},
- open: {
- tooltip: "Открыть в новой вкладке",
+ },
+ file_download: {
+ tooltip: {
+ image: "Скачать картинку",
+ video: "Скачать видео",
+ audio: "Скачать аудио",
+ file: "Скачать файл",
},
- form: {
- title_placeholder: "Изменить заголовок",
- url_placeholder: "Изменить URL",
+ },
+ file_delete: {
+ tooltip: {
+ image: "Удалить картинку",
+ video: "Удалить видео",
+ audio: "Удалить аудио",
+ file: "Скачать файл",
},
},
- generic: {
- ctrl_shortcut: "Ctrl",
+ file_preview_toggle: {
+ tooltip: "Переключить предварительный просмотр",
+ },
+ nest: {
+ tooltip: "Сдвинуть вправо",
+ secondary_tooltip: "Tab",
+ },
+ unnest: {
+ tooltip: "Сдвинуть влево",
+ secondary_tooltip: "Shift+Tab",
+ },
+ align_left: {
+ tooltip: "Текст по левому краю",
+ },
+ align_center: {
+ tooltip: "Текст по середине",
+ },
+ align_right: {
+ tooltip: "Текст по правому краю",
+ },
+ align_justify: {
+ tooltip: "По середине текст",
+ },
+ },
+ file_panel: {
+ upload: {
+ title: "Загрузить",
+ file_placeholder: {
+ image: "Загрузить картинки",
+ video: "Загрузить видео",
+ audio: "Загрузить аудио",
+ file: "Загрузить файл",
+ },
+ upload_error: "Ошибка: не удалось загрузить",
+ },
+ embed: {
+ title: "Вставить",
+ embed_button: {
+ image: "Вставить картинку",
+ video: "Вставить видео",
+ audio: "Вставить аудио",
+ file: "Вставить файл",
+ },
+ url_placeholder: "Введите URL",
+ },
+ },
+ link_toolbar: {
+ delete: {
+ tooltip: "Удалить ссылку",
+ },
+ edit: {
+ text: "Изменить ссылку",
+ tooltip: "Редактировать",
+ },
+ open: {
+ tooltip: "Открыть в новой вкладке",
+ },
+ form: {
+ title_placeholder: "Изменить заголовок",
+ url_placeholder: "Изменить URL",
},
- };
-
\ No newline at end of file
+ },
+ generic: {
+ ctrl_shortcut: "Ctrl",
+ },
+};
diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts
index 054076f321..f188196ddf 100644
--- a/packages/core/src/i18n/locales/vi.ts
+++ b/packages/core/src/i18n/locales/vi.ts
@@ -97,6 +97,19 @@ export const vi: Dictionary = {
aliases: ["tep", "tai-len", "nhung", "media", "url"],
group: "Phương tiện",
},
+ emoji: {
+ title: "Biểu tượng cảm xúc",
+ subtext: "Dùng để chèn biểu tượng cảm xúc",
+ aliases: [
+ "biểu tượng cảm xúc",
+ "emoji",
+ "emoticon",
+ "cảm xúc expression",
+ "khuôn mặt",
+ "face",
+ ],
+ group: "Khác",
+ },
},
placeholders: {
default: "Nhập văn bản hoặc gõ '/' để thêm định dạng",
diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts
index eb9364a89d..6c28356597 100644
--- a/packages/core/src/i18n/locales/zh.ts
+++ b/packages/core/src/i18n/locales/zh.ts
@@ -130,6 +130,20 @@ export const zh: Dictionary = {
aliases: ["文件", "上传", "file", "嵌入", "媒体", "url"],
group: "媒体",
},
+ emoji: {
+ title: "表情符号",
+ subtext: "用于插入表情符号",
+ aliases: [
+ "表情符号",
+ "emoji",
+ "face",
+ "emote",
+ "表情",
+ "表情表达",
+ "表情",
+ ],
+ group: "其他",
+ },
},
placeholders: {
default: "输入 '/' 以使用命令",
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 88b0f6186a..730d5ba8df 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -22,8 +22,10 @@ export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin";
export * from "./extensions/LinkToolbar/LinkToolbarPlugin";
export * from "./extensions/SideMenu/SideMenuPlugin";
export * from "./extensions/SuggestionMenu/DefaultSuggestionItem";
+export * from "./extensions/SuggestionMenu/DefaultGridSuggestionItem";
export * from "./extensions/SuggestionMenu/SuggestionPlugin";
export * from "./extensions/SuggestionMenu/getDefaultSlashMenuItems";
+export * from "./extensions/SuggestionMenu/getDefaultEmojiPickerItems";
export * from "./extensions/TableHandles/TableHandlesPlugin";
export * from "./schema";
export * from "./util/browser";
diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx
index 142a2db836..94c423ff2b 100644
--- a/packages/mantine/src/index.tsx
+++ b/packages/mantine/src/index.tsx
@@ -19,7 +19,10 @@ import {
applyBlockNoteCSSVariablesFromTheme,
removeBlockNoteCSSVariables,
} from "./BlockNoteTheme";
-import { TextInput } from "./form/TextInput";
+import { GridSuggestionMenu } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenu";
+import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem";
+import { GridSuggestionMenuItem } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem";
+import { GridSuggestionMenuLoader } from "./suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader";
import {
Menu,
MenuDivider,
@@ -42,10 +45,10 @@ import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem";
import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel";
import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader";
import { TableHandle } from "./tableHandle/TableHandle";
+import { TextInput } from "./form/TextInput";
import { Toolbar } from "./toolbar/Toolbar";
import { ToolbarButton } from "./toolbar/ToolbarButton";
import { ToolbarSelect } from "./toolbar/ToolbarSelect";
-
import "./style.css";
export * from "./BlockNoteTheme";
@@ -64,6 +67,12 @@ export const components: Components = {
TabPanel: PanelTab,
TextInput: PanelTextInput,
},
+ GridSuggestionMenu: {
+ Root: GridSuggestionMenu,
+ Item: GridSuggestionMenuItem,
+ EmptyItem: GridSuggestionMenuEmptyItem,
+ Loader: GridSuggestionMenuLoader,
+ },
LinkToolbar: {
Root: Toolbar,
Button: ToolbarButton,
diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css
index 92a1f2b0ac..a436a81ba4 100644
--- a/packages/mantine/src/style.css
+++ b/packages/mantine/src/style.css
@@ -299,6 +299,7 @@
box-shadow: var(--bn-shadow-medium);
box-sizing: border-box;
color: var(--bn-colors-menu-text);
+ height: fit-content;
overflow-y: auto;
padding: 2px;
}
@@ -362,6 +363,52 @@
background-color: var(--bn-colors-side-menu);
}
+.bn-mantine .bn-grid-suggestion-menu {
+ background: var(--bn-colors-menu-background);
+ border-radius: var(--bn-border-radius-large);
+ box-shadow: var(--bn-shadow-medium);
+ display: grid;
+ gap: 7px;
+ height: fit-content;
+ justify-items: center;
+ max-height: min(500px, 100%);
+ overflow-y: auto;
+ padding: 20px;
+}
+
+.bn-mantine .bn-grid-suggestion-menu-item {
+ align-items: center;
+ border-radius: var(--bn-border-radius-large);
+ cursor: pointer;
+ display: flex;
+ font-size: 24px;
+ height: 32px;
+ justify-content: center;
+ margin: 2px;
+ padding: 4px;
+ width: 32px;
+}
+
+.bn-mantine .bn-grid-suggestion-menu-item[aria-selected="true"],
+.bn-mantine .bn-grid-suggestion-menu-item:hover {
+ background-color: var(--bn-colors-hovered-background);
+}
+
+.bn-mantine .bn-grid-suggestion-menu-empty-item,
+.bn-mantine .bn-grid-suggestion-menu-loader{
+ align-items: center;
+ color: var(--bn-colors-menu-text);
+ display: flex;
+ font-size: 14px;
+ font-weight: 500;
+ height: 32px;
+ justify-content: center;
+}
+
+.bn-mantine .bn-grid-suggestion-menu-loader span {
+ background-color: var(--bn-colors-side-menu);
+}
+
/* Side Menu styling */
.bn-mantine .bn-side-menu {
background-color: transparent;
diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx
new file mode 100644
index 0000000000..b61454efe3
--- /dev/null
+++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenu.tsx
@@ -0,0 +1,23 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenu = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Root"]
+>((props, ref) => {
+ const { className, children, id, columns, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx
new file mode 100644
index 0000000000..fafb9fa1d0
--- /dev/null
+++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuEmptyItem.tsx
@@ -0,0 +1,25 @@
+import { Group as MantineGroup } from "@mantine/core";
+
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenuEmptyItem = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["EmptyItem"]
+>((props, ref) => {
+ const { className, children, columns, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+
+ {children}
+
+
+ );
+});
diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
new file mode 100644
index 0000000000..ded689a9cb
--- /dev/null
+++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuItem.tsx
@@ -0,0 +1,42 @@
+import { mergeRefs } from "@mantine/hooks";
+
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps, elementOverflow } from "@blocknote/react";
+import { forwardRef, useEffect, useRef } from "react";
+
+export const GridSuggestionMenuItem = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Item"]
+>((props, ref) => {
+ const { className, isSelected, onClick, item, id, ...rest } = props;
+
+ assertEmpty(rest);
+
+ const itemRef = useRef(null);
+
+ useEffect(() => {
+ if (!itemRef.current || !isSelected) {
+ return;
+ }
+
+ const overflow = elementOverflow(itemRef.current);
+
+ if (overflow === "top") {
+ itemRef.current.scrollIntoView(true);
+ } else if (overflow === "bottom") {
+ itemRef.current.scrollIntoView(false);
+ }
+ }, [isSelected]);
+
+ return (
+
+ {item.icon}
+
+ );
+});
diff --git a/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx
new file mode 100644
index 0000000000..42dda5d583
--- /dev/null
+++ b/packages/mantine/src/suggestionMenu/gridSuggestionMenu/GridSuggestionMenuLoader.tsx
@@ -0,0 +1,28 @@
+import { Loader as MantineLoader } from "@mantine/core";
+
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+import { forwardRef } from "react";
+
+export const GridSuggestionMenuLoader = forwardRef<
+ HTMLDivElement,
+ ComponentProps["GridSuggestionMenu"]["Loader"]
+>((props, ref) => {
+ const {
+ className,
+ children, // unused, using "dots" instead
+ columns,
+ ...rest
+ } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ );
+});
diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenu.tsx
new file mode 100644
index 0000000000..12a816eef4
--- /dev/null
+++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenu.tsx
@@ -0,0 +1,75 @@
+import { useMemo } from "react";
+
+import { useComponentsContext } from "../../../editor/ComponentsContext";
+import { useDictionary } from "../../../i18n/dictionary";
+import {
+ DefaultReactGridSuggestionItem,
+ GridSuggestionMenuProps,
+} from "./types";
+
+export function GridSuggestionMenu(
+ props: GridSuggestionMenuProps
+) {
+ const Components = useComponentsContext()!;
+ const dict = useDictionary();
+
+ const { items, loadingState, selectedIndex, onItemClick, columns } = props;
+
+ const loader =
+ loadingState === "loading-initial" || loadingState === "loading" ? (
+
+ {dict.suggestion_menu.loading}
+
+ ) : null;
+
+ const renderedItems = useMemo(() => {
+ // let currentGroup: string | undefined = undefined;
+ const renderedItems = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ // if (item.group !== currentGroup) {
+ // currentGroup = item.group;
+ // renderedItems.push(
+ //
+ // {currentGroup}
+ //
+ // );
+ // }
+
+ renderedItems.push(
+