diff --git a/.claude/skills/create-example/SKILL.md b/.claude/skills/create-example/SKILL.md new file mode 100644 index 0000000000..dbf95af451 --- /dev/null +++ b/.claude/skills/create-example/SKILL.md @@ -0,0 +1,72 @@ +--- +name: create-example +description: Creates a new example under the `/examples` directory. +--- + +This skill provides instructions on how to create a BlockNote editor example correctly. + +# Creating an example root directory + +Under the `/examples` directory, each subdirectory is a category of examples. It's name consists of a 2 digit index, followed by a dash and the category name. + +Each of these contains another set of subdirectories, where each one contains a single example. The naming of these is the same, but the category name is swapped for the example name. + +Based on the user's prompt, the most relevant category should be chosen, and a new directory for the example should be created. The index in the example directory's name should be the lowest unused one to avoid large diffs from having to reorder & rename the existing example directories. It is very unlikely that a new category directory should need to be created for the new example, but it should use the same convention. + +# Source & metadata files + +## Source files + +Any source files must be inside a `/src` directory at the root of the example directory. Within these, there must also be an `App.tsx` file, which default exports a React component. This component is responsible for rendering the entire example. + +## Metadata files + +There are two files containing metadata that must also be added at the root of the example directory: + +`.bnexample.json` + +Contains all of the example's configuration. Here's an annotated example (from `/examples/03-ui-components/13-custom-ui/.bnexample.json`): + +``` + +"playground": true, + +"docs": true, + +"author": "matthewlipski", + +"tags": [ + "Advanced", + "Inline Content", + "UI Components", + "Block Side Menu", + "Formatting Toolbar", + "Suggestion Menus", + "Slash Menu", + "Appearance & Styling" +], + +"dependencies": { + "@mui/icons-material": "^5.16.1", + "@mui/material": "^5.16.1" +}, + +"pro": true +``` + +`README.md` + +A Markdown description of the example. Made of four parts: + +1. Heading with the example name. This does not necessarily need to be the same as the example directory name and can be more verbose. +2. Description of the example, which should be no longer than a paragraph of three sentences. +3. An optional "Try me out!" callout. Should be a single sentence instructing the user how to see the changes made to the editor in the example. +4. A list of relevant docs. These are mostly internal but may also refer to e.g. dependencies used in the example. + +See `/examples/07-collaboration/01-partykit` for reference on the exact markup of these sections. + +# Generated files + +Once the source & metadata files are done, `vp run gen` should be executed from the project root directory to auto-generate additional files in the example directory, as well as the playground & docs. + +One of the generated files is `package.json` in the example directory. If the `bnexample.json` specified any dependencies, these will be included here. Therefore, `vp install` should always be executed in the project root directory after, to ensure these are installed. diff --git a/.claude/skills/debug-skill/SKILL.md b/.claude/skills/debug-skill/SKILL.md new file mode 100644 index 0000000000..cc47080e23 --- /dev/null +++ b/.claude/skills/debug-skill/SKILL.md @@ -0,0 +1,70 @@ +--- +name: debug-skill +description: Instructions for navigating and debugging BlockNote in a browser. Shows how to open specific menus & toolbars, as well select content. Should be used when prompted to fix a bug that requires inspecting the editor's appearance or rendered HTML. +--- + +# General loop + +When fixing a bug, the following feedback loop should be used. + +1. Apply a code change that you think will fix the issue. +2. Test the change in a browser environment. +3. Take screenshots to verify that the issue is fixed. +4. Repeat until the bug is indeed fixed. + +# Browser environment + +Before starting up a browser environment, you need to ensure the dev server is running. This can be done by checking if port 5173 is in use. If it isn't, running `vp run dev` at the project root will start the server. + +The Playwright CLI should be used for the browser environment. It can be used to navigate to the dev server and programmatically issue mouse clicks/keyboard inputs. If not installed, stop what you're doing and notify the user to install it. + +# Selecting an example + +After navigating to `localhost:5173`, an example must be selected. These are listed in the navbar (`mantine-AppShell-navbar` CSS class). The "Default Schema Showcase" should be selected, unless stated otherwise by the user. + +Each example will contain a BlockNote editor, and possibly additional elements like text fields or static toolbars. + +# Editor HTML structure + +Below is a list of elements that make up a BlockNote editor. This is helpful for mapping BlockNote concepts to what's actually visible in the browser. The nesting of the list items is representative of how the corresponding elements are nested in the rendered HTML. The elements are referenced by their main CSS class. + +- `bn-container`: Wrapper element for the editor. + - `bn-editor`: Root element for the BlockNote editor. + - `bn-block-group`: Root container for blocks. + - `bn-block-outer`: Wrapper element for a block. + - `bn-block`: Root element for a block. + - `bn-block-content`: Container element for all content rendered by the block itself. Also renders a `data-content-type` attribute which stores the block's type, and additional `data-*` for every non-default prop that the block has. + - `bn-inline-content`: Container element for user-editable rich text within a block. Note that not all blocks will contain this element. + - `bn-block-group`: Container for nested blocks. Note that if a block doesn't contain nested blocks, it won't have this element. + - `bn-block-column-list`: Container element for columns. + - `bn-block-column`: Column element containing blocks. + +Each element only appears once in its parent, except `bn-block-outer` and `bn-block-column-list`, which can appear multiple times. + +Each `bn-block-group` and `bn-block-column` also contain `bn-block-outer` elements. These are not listed as they can be nested to an arbitrary depth. + +Note that additional UI elements like menus and toolbars are mounted in a portal attached to the `body`. + +# Keyboard navigation + +Assume you are on a machine running macOS. You can use the following key combinations to navigate through the editor and create selections: + +- Left/Right Arrow: Moves the text cursor back/forward one character. +- Up/Down Arrow: Moves the text cursor to the previous/next block. +- Option + Left/Right Arrow: Moves the text cursor to the start/end of the current word. If already at the start/end of a word, moves it to the start/end of the previous/next one instead. +- Cmd + Left/Right Arrow: Moves the text cursor to the start/end of the line. +- Cmd + Up/Down Arrow: Moves the text cursor to the start/end of the document. + +Each of these can also be used with Shift to create/extend a selection instead of just moving the cursor. + +It is extremely important to note that these key combinations are only relevant for debugging and NOT for writing end-to-end tests. While Playwright is used for both, tests run in a Linux environment which has different bindings for keyboard navigation. + +# Opening menus & toolbars + +Here are the most often used UI elements, and how to find/open them. + +- **Formatting toolbar**: Create a selection using the keyboard and look for an element with the `bn-formatting-toolbar` CSS class. Buttons/dropdowns within it should be interacted with using the mouse instead. The `data-test` attribute will inform you what a given button or dropdown is for. Press escape to dismiss the toolbar. +- **Side menu**: Hover a block with the mouse, i.e. a `bn-block` element, and look for an element with the `bn-side-menu` CSS class. Unless specified otherwise, it contains a button to add a block ("Add block" ARIA label) and a drag handle which opens a menu on click ("Open block menu" ARIA label). Typing in the editor or moving the mouse cursor above/below it will hide the side menu, unless the drag handle menu is open. Then, it's "frozen" until dismissed by an outside click or pressing Escape. +- **Slash menu**: Type the "/" key while in a block and look for an element with the `bn-suggestion-menu` CSS class. It contains a list of items with the `bn-suggestion-menu-item` CSS class. While the menu is open, the up/down arrows navigate through items instead of moving the text cursor. Items can be triggered with a mouse click or pressing Enter while selected. Each item will convert the type of the current block to one of a given type, if it's empty. Otherwise, it will create a new block below with that type. The `bn-suggestion-menu-item-title` element's text content will indicate the new type. Pressing Escape closes the menu. +- **Link toolbar**: Hover a link in a block (anchor element within a `bn-inline-content` element), or move the text selection inside it using the arrow keys, and look for an element with the `bn-link-toolbar` CSS class. Unless specified otherwise, it contains three buttons. The first has the text, "Edit link". On click, it opens a popup to edit the link text and URL. You can locate these inputs with the "Edit title" and "Edit URL" placeholders. The other two buttons are for opening the link in a new tab ("Open in new tab" ARIA label) and deleting the link ("Remove link" ARIA label). If the toolbar was opened via mouse hover, moving the mouse off of the link or toolbar will close it after half a second. Otherwise, moving the text cursor outside the link will close the toolbar. It can also be dismissed by pressing Escape. +- **File panel**: After creating a file, image, video, or audio block, it will render a button with the text "Add file" (`bn-add-file-button` CSS class). Clicking the button will open the file panel. When the block is created using the slash menu (typically the case), the file panel will be open immediately. It always has an "Embed" tab (`data-test="embed-tab"` attribute). While this tab is selected, the file panel displays an input for the file URL ("Enter URL" placeholder) and "Embed file" button. For some examples, an "Upload" tab (`data-test="upload-tab"` attribute) will also be present. While it's selected, the file panel will display a file input (`data-test="upload-input"` attribute). After embedding/uploading a file, the block will render said file instead of displaying the "Add file" button. diff --git a/.claude/skills/playwright-cli/SKILL.md b/.claude/skills/playwright-cli/SKILL.md new file mode 100644 index 0000000000..fe30992ad9 --- /dev/null +++ b/.claude/skills/playwright-cli/SKILL.md @@ -0,0 +1,394 @@ +--- +name: playwright-cli +description: Automate browser interactions, test web pages and work with Playwright tests. +allowed-tools: Bash(playwright-cli:*) Bash(npx:*) Bash(npm:*) +--- + +# Browser Automation with playwright-cli + +## Quick start + +```bash +# open new browser +playwright-cli open +# navigate to a page +playwright-cli goto https://playwright.dev +# interact with the page using refs from the snapshot +playwright-cli click e15 +playwright-cli type "page.click" +playwright-cli press Enter +# take a screenshot (rarely used, as snapshot is more common) +playwright-cli screenshot +# close the browser +playwright-cli close +``` + +## Commands + +### Core + +```bash +playwright-cli open +# open and navigate right away +playwright-cli open https://example.com/ +playwright-cli goto https://playwright.dev +playwright-cli type "search query" +playwright-cli click e3 +playwright-cli dblclick e7 +# --submit presses Enter after filling the element +playwright-cli fill e5 "user@example.com" --submit +playwright-cli drag e2 e8 +# drop files or data onto an element (from outside the page) +playwright-cli drop e4 --path=./image.png +playwright-cli drop e4 --data="text/plain=hello world" +playwright-cli hover e4 +playwright-cli select e9 "option-value" +playwright-cli upload ./document.pdf +playwright-cli check e12 +playwright-cli uncheck e12 +playwright-cli snapshot +playwright-cli eval "document.title" +playwright-cli eval "el => el.textContent" e5 +# get element id, class, or any attribute not visible in the snapshot +playwright-cli eval "el => el.id" e5 +playwright-cli eval "el => el.getAttribute('data-testid')" e5 +playwright-cli dialog-accept +playwright-cli dialog-accept "confirmation text" +playwright-cli dialog-dismiss +playwright-cli resize 1920 1080 +playwright-cli close +``` + +### Navigation + +```bash +playwright-cli go-back +playwright-cli go-forward +playwright-cli reload +``` + +### Keyboard + +```bash +playwright-cli press Enter +playwright-cli press ArrowDown +playwright-cli keydown Shift +playwright-cli keyup Shift +``` + +### Mouse + +```bash +playwright-cli mousemove 150 300 +playwright-cli mousedown +playwright-cli mousedown right +playwright-cli mouseup +playwright-cli mouseup right +playwright-cli mousewheel 0 100 +``` + +### Save as + +Also resize large screenshots with sips (native macOS tool) if possible. + +Screenshots should be saved to `.claude/skills/playwright-cli/screenshots/` in the workspace root. + +```bash +playwright-cli screenshot +playwright-cli screenshot e5 +playwright-cli screenshot --filename=page.png +playwright-cli pdf --filename=page.pdf +``` + +### Tabs + +```bash +playwright-cli tab-list +playwright-cli tab-new +playwright-cli tab-new https://example.com/page +playwright-cli tab-close +playwright-cli tab-close 2 +playwright-cli tab-select 0 +``` + +### Storage + +```bash +playwright-cli state-save +playwright-cli state-save auth.json +playwright-cli state-load auth.json + +# Cookies +playwright-cli cookie-list +playwright-cli cookie-list --domain=example.com +playwright-cli cookie-get session_id +playwright-cli cookie-set session_id abc123 +playwright-cli cookie-set session_id abc123 --domain=example.com --httpOnly --secure +playwright-cli cookie-delete session_id +playwright-cli cookie-clear + +# LocalStorage +playwright-cli localstorage-list +playwright-cli localstorage-get theme +playwright-cli localstorage-set theme dark +playwright-cli localstorage-delete theme +playwright-cli localstorage-clear + +# SessionStorage +playwright-cli sessionstorage-list +playwright-cli sessionstorage-get step +playwright-cli sessionstorage-set step 3 +playwright-cli sessionstorage-delete step +playwright-cli sessionstorage-clear +``` + +### Network + +```bash +playwright-cli route "**/*.jpg" --status=404 +playwright-cli route "https://api.example.com/**" --body='{"mock": true}' +playwright-cli route-list +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +### DevTools + +```bash +playwright-cli console +playwright-cli console warning +playwright-cli requests +playwright-cli request 5 +playwright-cli run-code "async page => await page.context().grantPermissions(['geolocation'])" +playwright-cli run-code --filename=script.js +playwright-cli tracing-start +playwright-cli tracing-stop +playwright-cli video-start video.webm +playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000 +playwright-cli video-stop + +# launch the dashboard for UI review / design feedback — user annotates the page, you receive the annotated screenshot, snapshot, and notes +playwright-cli show --annotate + +# generate a Playwright locator for an element from its ref or selector +playwright-cli generate-locator e5 --raw + +# show a persistent highlight overlay for an element, optionally with a custom style +playwright-cli highlight e5 +playwright-cli highlight e5 --style="outline: 3px dashed red" +# hide a single element highlight, or all page highlights when no target is given +playwright-cli highlight e5 --hide +playwright-cli highlight --hide +``` + +## Raw output + +The global `--raw` option strips page status, generated code, and snapshot sections from the output, returning only the result value. Use it to pipe command output into other tools. Commands that don't produce output return nothing. + +```bash +playwright-cli --raw eval "JSON.stringify(performance.timing)" | jq '.loadEventEnd - .navigationStart' +playwright-cli --raw eval "JSON.stringify([...document.querySelectorAll('a')].map(a => a.href))" > links.json +playwright-cli --raw snapshot > before.yml +playwright-cli click e5 +playwright-cli --raw snapshot > after.yml +diff before.yml after.yml +TOKEN=$(playwright-cli --raw cookie-get session_id) +playwright-cli --raw localstorage-get theme +``` + +For structured output wrapping every reply as JSON, pass --json + +```bash +playwright-cli list --json +``` + +## Open parameters + +```bash +# Use specific browser when creating session +playwright-cli open --browser=chrome +playwright-cli open --browser=firefox +playwright-cli open --browser=webkit +playwright-cli open --browser=msedge + +# Use persistent profile (by default profile is in-memory) +playwright-cli open --persistent +# Use persistent profile with custom directory +playwright-cli open --profile=/path/to/profile + +# Connect to browser via Playwright Extension +playwright-cli attach --extension=chrome + +# Connect to a running Chrome or Edge by channel name +playwright-cli attach --cdp=chrome +playwright-cli attach --cdp=msedge + +# Connect to a running browser via CDP endpoint +playwright-cli attach --cdp=http://localhost:9222 + +# Start with config file +playwright-cli open --config=my-config.json + +# Close the browser +playwright-cli close +# Detach from an attached browser (leaves the external browser running) +playwright-cli -s=msedge detach +# Delete user data for the default session +playwright-cli delete-data +``` + +## Snapshots + +After each command, playwright-cli provides a snapshot of the current browser state. + +```bash +> playwright-cli goto https://example.com +### Page +- Page URL: https://example.com/ +- Page Title: Example Domain +### Snapshot +[Snapshot](.playwright-cli/page-2026-02-14T19-22-42-679Z.yml) +``` + +You can also take a snapshot on demand using `playwright-cli snapshot` command. All the options below can be combined as needed. + +```bash +# default - save to a file with timestamp-based name +playwright-cli snapshot + +# save to file, use when snapshot is a part of the workflow result +playwright-cli snapshot --filename=after-click.yaml + +# snapshot an element instead of the whole page +playwright-cli snapshot "#main" + +# limit snapshot depth for efficiency, take a partial snapshot afterwards +playwright-cli snapshot --depth=4 +playwright-cli snapshot e34 + +# include each element's bounding box as [box=x,y,width,height] +playwright-cli snapshot --boxes +``` + +## Targeting elements + +By default, use refs from the snapshot to interact with page elements. + +```bash +# get snapshot with refs +playwright-cli snapshot + +# interact using a ref +playwright-cli click e15 +``` + +You can also use css selectors or Playwright locators. + +```bash +# css selector +playwright-cli click "#main > button.submit" + +# role locator +playwright-cli click "getByRole('button', { name: 'Submit' })" + +# test id +playwright-cli click "getByTestId('submit-button')" +``` + +## Browser Sessions + +```bash +# create new browser session named "mysession" with persistent profile +playwright-cli -s=mysession open example.com --persistent +# same with manually specified profile directory (use when requested explicitly) +playwright-cli -s=mysession open example.com --profile=/path/to/profile +playwright-cli -s=mysession click e6 +playwright-cli -s=mysession close # stop a named browser +playwright-cli -s=mysession delete-data # delete user data for persistent session + +playwright-cli list +# Close all browsers +playwright-cli close-all +# Forcefully kill all browser processes +playwright-cli kill-all +``` + +## Installation + +If global `playwright-cli` command is not available, try a local version via `npx playwright-cli`: + +```bash +npx --no-install playwright-cli --version +``` + +When local version is available, use `npx playwright-cli` in all commands. Otherwise, install `playwright-cli` as a global command: + +```bash +npm install -g @playwright/cli@latest +``` + +## Example: Form submission + +```bash +playwright-cli open https://example.com/form +playwright-cli snapshot + +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Multi-tab workflow + +```bash +playwright-cli open https://example.com +playwright-cli tab-new https://example.com/other +playwright-cli tab-list +playwright-cli tab-select 0 +playwright-cli snapshot +playwright-cli close +``` + +## Example: Debugging with DevTools + +```bash +playwright-cli open https://example.com +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli console +playwright-cli requests +playwright-cli close +``` + +```bash +playwright-cli open https://example.com +playwright-cli tracing-start +playwright-cli click e4 +playwright-cli fill e7 "test" +playwright-cli tracing-stop +playwright-cli close +``` + +## Example: Interactive session + +Ask the user for UI review or design feedback. The user draws boxes on the live page and types comments; you receive the annotated screenshot, the snapshot of the marked region, and the user's notes. Use this whenever the user asks for "UI review", "design feedback", or to "ask the user what they think / want / mean": + +```bash +playwright-cli open https://example.com +playwright-cli show --annotate +``` + +## Specific tasks + +- **Running and Debugging Playwright tests** [references/playwright-tests.md](references/playwright-tests.md) +- **Request mocking** [references/request-mocking.md](references/request-mocking.md) +- **Running Playwright code** [references/running-code.md](references/running-code.md) +- **Browser session management** [references/session-management.md](references/session-management.md) +- **Spec-driven testing (plan / generate / heal)** [references/spec-driven-testing.md](references/spec-driven-testing.md) +- **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md) +- **Test generation** [references/test-generation.md](references/test-generation.md) +- **Tracing** [references/tracing.md](references/tracing.md) +- **Video recording** [references/video-recording.md](references/video-recording.md) +- **Inspecting element attributes** [references/element-attributes.md](references/element-attributes.md) diff --git a/.claude/skills/playwright-cli/references/element-attributes.md b/.claude/skills/playwright-cli/references/element-attributes.md new file mode 100644 index 0000000000..4e9fa6b991 --- /dev/null +++ b/.claude/skills/playwright-cli/references/element-attributes.md @@ -0,0 +1,23 @@ +# Inspecting Element Attributes + +When the snapshot doesn't show an element's `id`, `class`, `data-*` attributes, or other DOM properties, use `eval` to inspect them. + +## Examples + +```bash +playwright-cli snapshot +# snapshot shows a button as e7 but doesn't reveal its id or data attributes + +# get the element's id +playwright-cli eval "el => el.id" e7 + +# get all CSS classes +playwright-cli eval "el => el.className" e7 + +# get a specific attribute +playwright-cli eval "el => el.getAttribute('data-testid')" e7 +playwright-cli eval "el => el.getAttribute('aria-label')" e7 + +# get a computed style property +playwright-cli eval "el => getComputedStyle(el).display" e7 +``` diff --git a/.claude/skills/playwright-cli/references/playwright-tests.md b/.claude/skills/playwright-cli/references/playwright-tests.md new file mode 100644 index 0000000000..bec2ec90e4 --- /dev/null +++ b/.claude/skills/playwright-cli/references/playwright-tests.md @@ -0,0 +1,39 @@ +# Running Playwright Tests + +To run Playwright tests, use the `npx playwright test` command, or a package manager script. To avoid opening the interactive html report, use `PLAYWRIGHT_HTML_OPEN=never` environment variable. + +```bash +# Run all tests +PLAYWRIGHT_HTML_OPEN=never npx playwright test + +# Run all tests through a custom npm script +PLAYWRIGHT_HTML_OPEN=never npm run special-test-command +``` + +# Debugging Playwright Tests + +To debug a failing Playwright test, run it with `--debug=cli` option. This command will pause the test at the start and print the debugging instructions. + +**IMPORTANT**: run the command in the background and check the output until "Debugging Instructions" is printed. Make sure to stop the command after you have finished. + +Once instructions containing a session name are printed, use `playwright-cli` to attach the session and explore the page. + +```bash +# Run the test +PLAYWRIGHT_HTML_OPEN=never npx playwright test --debug=cli +# ... +# ... debugging instructions for "tw-abcdef" session ... +# ... + +# Attach to the test +playwright-cli attach tw-abcdef +``` + +Keep the test running in the background while you explore and look for a fix. +The test is paused at the start, so you should step over or pause at a particular location +where the problem is most likely to be. + +Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. +This code appears in the output and can be copied directly into the test. Most of the time, a specific locator or an expectation should be updated, but it could also be a bug in the app. Use your judgement. + +After fixing the test, stop the background test run. Rerun to check that test passes. diff --git a/.claude/skills/playwright-cli/references/request-mocking.md b/.claude/skills/playwright-cli/references/request-mocking.md new file mode 100644 index 0000000000..9005fda67d --- /dev/null +++ b/.claude/skills/playwright-cli/references/request-mocking.md @@ -0,0 +1,87 @@ +# Request Mocking + +Intercept, mock, modify, and block network requests. + +## CLI Route Commands + +```bash +# Mock with custom status +playwright-cli route "**/*.jpg" --status=404 + +# Mock with JSON body +playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json + +# Mock with custom headers +playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value" + +# Remove headers from requests +playwright-cli route "**/*" --remove-header=cookie,authorization + +# List active routes +playwright-cli route-list + +# Remove a route or all routes +playwright-cli unroute "**/*.jpg" +playwright-cli unroute +``` + +## URL Patterns + +``` +**/api/users - Exact path match +**/api/*/details - Wildcard in path +**/*.{png,jpg,jpeg} - Match file extensions +**/search?q=* - Match query parameters +``` + +## Advanced Mocking with run-code + +For conditional responses, request body inspection, response modification, or delays: + +### Conditional Response Based on Request + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/login', route => { + const body = route.request().postDataJSON(); + if (body.username === 'admin') { + route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) }); + } else { + route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) }); + } + }); +}" +``` + +### Modify Real Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/user', async route => { + const response = await route.fetch(); + const json = await response.json(); + json.isPremium = true; + await route.fulfill({ response, json }); + }); +}" +``` + +### Simulate Network Failures + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/offline', route => route.abort('internetdisconnected')); +}" +# Options: connectionrefused, timedout, connectionreset, internetdisconnected +``` + +### Delayed Response + +```bash +playwright-cli run-code "async page => { + await page.route('**/api/slow', async route => { + await new Promise(r => setTimeout(r, 3000)); + route.fulfill({ body: JSON.stringify({ data: 'loaded' }) }); + }); +}" +``` diff --git a/.claude/skills/playwright-cli/references/running-code.md b/.claude/skills/playwright-cli/references/running-code.md new file mode 100644 index 0000000000..06645ec124 --- /dev/null +++ b/.claude/skills/playwright-cli/references/running-code.md @@ -0,0 +1,240 @@ +# Running Custom Playwright Code + +Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands. + +## Syntax + +```bash +playwright-cli run-code "async page => { + // Your Playwright code here + // Access page.context() for browser context operations +}" +``` + +You can also load the function from a file: + +```bash +playwright-cli run-code --filename=./my-script.js +``` + +The code must be a single function expression, it is wrapped in `(...)` and evaluated. +import/export/require syntax is not supported. + +## Geolocation + +```bash +# Grant geolocation permission and set location +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); +}" + +# Set location to London +playwright-cli run-code "async page => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 }); +}" + +# Clear geolocation override +playwright-cli run-code "async page => { + await page.context().clearPermissions(); +}" +``` + +## Permissions + +```bash +# Grant multiple permissions +playwright-cli run-code "async page => { + await page.context().grantPermissions([ + 'geolocation', + 'notifications', + 'camera', + 'microphone' + ]); +}" + +# Grant permissions for specific origin +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read'], { + origin: 'https://example.com' + }); +}" +``` + +## Media Emulation + +```bash +# Emulate dark color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'dark' }); +}" + +# Emulate light color scheme +playwright-cli run-code "async page => { + await page.emulateMedia({ colorScheme: 'light' }); +}" + +# Emulate reduced motion +playwright-cli run-code "async page => { + await page.emulateMedia({ reducedMotion: 'reduce' }); +}" + +# Emulate print media +playwright-cli run-code "async page => { + await page.emulateMedia({ media: 'print' }); +}" +``` + +## Wait Strategies + +```bash +# Wait for network idle +playwright-cli run-code "async page => { + await page.waitForLoadState('networkidle'); +}" + +# Wait for specific element +playwright-cli run-code "async page => { + await page.locator('.loading').waitFor({ state: 'hidden' }); +}" + +# Wait for function to return true +playwright-cli run-code "async page => { + await page.waitForFunction(() => window.appReady === true); +}" + +# Wait with timeout +playwright-cli run-code "async page => { + await page.locator('.result').waitFor({ timeout: 10000 }); +}" +``` + +## Frames and Iframes + +```bash +# Work with iframe +playwright-cli run-code "async page => { + const frame = page.locator('iframe#my-iframe').contentFrame(); + await frame.locator('button').click(); +}" + +# Get all frames +playwright-cli run-code "async page => { + const frames = page.frames(); + return frames.map(f => f.url()); +}" +``` + +## File Downloads + +```bash +# Handle file download +playwright-cli run-code "async page => { + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('link', { name: 'Download' }).click(); + const download = await downloadPromise; + await download.saveAs('./downloaded-file.pdf'); + return download.suggestedFilename(); +}" +``` + +## Clipboard + +```bash +# Read clipboard (requires permission) +playwright-cli run-code "async page => { + await page.context().grantPermissions(['clipboard-read']); + return await page.evaluate(() => navigator.clipboard.readText()); +}" + +# Write to clipboard +playwright-cli run-code "async page => { + await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!'); +}" +``` + +## Page Information + +```bash +# Get page title +playwright-cli run-code "async page => { + return await page.title(); +}" + +# Get current URL +playwright-cli run-code "async page => { + return page.url(); +}" + +# Get page content +playwright-cli run-code "async page => { + return await page.content(); +}" + +# Get viewport size +playwright-cli run-code "async page => { + return page.viewportSize(); +}" +``` + +## JavaScript Execution + +```bash +# Execute JavaScript and return result +playwright-cli run-code "async page => { + return await page.evaluate(() => { + return { + userAgent: navigator.userAgent, + language: navigator.language, + cookiesEnabled: navigator.cookieEnabled + }; + }); +}" + +# Pass arguments to evaluate +playwright-cli run-code "async page => { + const multiplier = 5; + return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier); +}" +``` + +## Error Handling + +```bash +# Try-catch in run-code +playwright-cli run-code "async page => { + try { + await page.getByRole('button', { name: 'Submit' }).click({ timeout: 1000 }); + return 'clicked'; + } catch (e) { + return 'element not found'; + } +}" +``` + +## Complex Workflows + +```bash +# Login and save state +playwright-cli run-code "async page => { + await page.goto('https://example.com/login'); + await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + await page.getByRole('textbox', { name: 'Password' }).fill('secret'); + await page.getByRole('button', { name: 'Sign in' }).click(); + await page.waitForURL('**/dashboard'); + await page.context().storageState({ path: 'auth.json' }); + return 'Login successful'; +}" + +# Scrape data from multiple pages +playwright-cli run-code "async page => { + const results = []; + for (let i = 1; i <= 3; i++) { + await page.goto(\`https://example.com/page/\${i}\`); + const items = await page.locator('.item').allTextContents(); + results.push(...items); + } + return results; +}" +``` diff --git a/.claude/skills/playwright-cli/references/session-management.md b/.claude/skills/playwright-cli/references/session-management.md new file mode 100644 index 0000000000..287e77fb7f --- /dev/null +++ b/.claude/skills/playwright-cli/references/session-management.md @@ -0,0 +1,226 @@ +# Browser Session Management + +Run multiple isolated browser sessions concurrently with state persistence. + +## Named Browser Sessions + +Use `-s` flag to isolate browser contexts: + +```bash +# Browser 1: Authentication flow +playwright-cli -s=auth open https://app.example.com/login + +# Browser 2: Public browsing (separate cookies, storage) +playwright-cli -s=public open https://example.com + +# Commands are isolated by browser session +playwright-cli -s=auth fill e1 "user@example.com" +playwright-cli -s=public snapshot +``` + +## Browser Session Isolation Properties + +Each browser session has independent: + +- Cookies +- LocalStorage / SessionStorage +- IndexedDB +- Cache +- Browsing history +- Open tabs + +## Browser Session Commands + +```bash +# List all browser sessions +playwright-cli list + +# Stop a browser session (close the browser) +playwright-cli close # stop the default browser +playwright-cli -s=mysession close # stop a named browser + +# Stop all browser sessions +playwright-cli close-all + +# Forcefully kill all daemon processes (for stale/zombie processes) +playwright-cli kill-all + +# Delete browser session user data (profile directory) +playwright-cli delete-data # delete default browser data +playwright-cli -s=mysession delete-data # delete named browser data +``` + +## Environment Variable + +Set a default browser session name via environment variable: + +```bash +export PLAYWRIGHT_CLI_SESSION="mysession" +playwright-cli open example.com # Uses "mysession" automatically +``` + +## Common Patterns + +### Concurrent Scraping + +```bash +#!/bin/bash +# Scrape multiple sites concurrently + +# Start all browsers +playwright-cli -s=site1 open https://site1.com & +playwright-cli -s=site2 open https://site2.com & +playwright-cli -s=site3 open https://site3.com & +wait + +# Take snapshots from each +playwright-cli -s=site1 snapshot +playwright-cli -s=site2 snapshot +playwright-cli -s=site3 snapshot + +# Cleanup +playwright-cli close-all +``` + +### A/B Testing Sessions + +```bash +# Test different user experiences +playwright-cli -s=variant-a open "https://app.com?variant=a" +playwright-cli -s=variant-b open "https://app.com?variant=b" + +# Compare +playwright-cli -s=variant-a screenshot +playwright-cli -s=variant-b screenshot +``` + +### Persistent Profile + +By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk: + +```bash +# Use persistent profile (auto-generated location) +playwright-cli open https://example.com --persistent + +# Use persistent profile with custom directory +playwright-cli open https://example.com --profile=/path/to/profile +``` + +## Attaching to a Running Browser + +Use `attach` to connect to a browser that is already running, instead of launching a new one. + +### Attach by channel name + +Connect to a running Chrome or Edge instance by its channel name. The browser must have remote debugging enabled — navigate to `chrome://inspect/#remote-debugging` in the target browser and check "Allow remote debugging for this browser instance". + +```bash +# Attach to Chrome +playwright-cli attach --cdp=chrome + +# Attach to Chrome Canary +playwright-cli attach --cdp=chrome-canary + +# Attach to Microsoft Edge +playwright-cli attach --cdp=msedge + +# Attach to Edge Dev +playwright-cli attach --cdp=msedge-dev +``` + +Supported channels: `chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`, `msedge-beta`, `msedge-dev`, `msedge-canary`. + +When `--session` is not provided, the session is named after the channel (e.g. `--cdp=msedge` creates a session called `msedge`), so parallel attaches to Chrome and Edge don't collide on `default`. Pass `--session=` to override. + +### Attach via CDP endpoint + +Connect to a browser that exposes a Chrome DevTools Protocol endpoint: + +```bash +playwright-cli attach --cdp=http://localhost:9222 +``` + +### Attach via browser extension + +Connect to a browser with the Playwright extension installed: + +```bash +playwright-cli attach --extension +``` + +### Detach + +Tear down an attached session without affecting the external browser: + +```bash +# Detach the default attached session +playwright-cli detach + +# Detach a specific attached session +playwright-cli -s=msedge detach +``` + +`detach` only works on sessions created via `attach`. For sessions created via `open`, use `close`. + +## Default Browser Session + +When `-s` is omitted, commands use the default browser session: + +```bash +# These use the same default browser session +playwright-cli open https://example.com +playwright-cli snapshot +playwright-cli close # Stops default browser +``` + +## Browser Session Configuration + +Configure a browser session with specific settings when opening: + +```bash +# Open with config file +playwright-cli open https://example.com --config=.playwright/my-cli.json + +# Open with specific browser +playwright-cli open https://example.com --browser=firefox + +# Open in headed mode +playwright-cli open https://example.com --headed + +# Open with persistent profile +playwright-cli open https://example.com --persistent +``` + +## Best Practices + +### 1. Name Browser Sessions Semantically + +```bash +# GOOD: Clear purpose +playwright-cli -s=github-auth open https://github.com +playwright-cli -s=docs-scrape open https://docs.example.com + +# AVOID: Generic names +playwright-cli -s=s1 open https://github.com +``` + +### 2. Always Clean Up + +```bash +# Stop browsers when done +playwright-cli -s=auth close +playwright-cli -s=scrape close + +# Or stop all at once +playwright-cli close-all + +# If browsers become unresponsive or zombie processes remain +playwright-cli kill-all +``` + +### 3. Delete Stale Browser Data + +```bash +# Remove old browser data to free disk space +playwright-cli -s=oldsession delete-data +``` diff --git a/.claude/skills/playwright-cli/references/spec-driven-testing.md b/.claude/skills/playwright-cli/references/spec-driven-testing.md new file mode 100644 index 0000000000..70f05135eb --- /dev/null +++ b/.claude/skills/playwright-cli/references/spec-driven-testing.md @@ -0,0 +1,308 @@ +# Spec-driven testing (plan → generate → heal) + +End-to-end workflow for authoring and maintaining Playwright tests using `playwright-cli`. The three sections below can be used independently: + +- **Planning** — explore the app, produce a spec file describing what to test. +- **Generate** — turn a spec into Playwright test files. Update the spec if it's vague or stale. +- **Heal** — diagnose failing tests, fix the code, reconcile the spec with reality. + +All three lean on the same mechanic: run `npx playwright test --debug=cli` in the background, then `playwright-cli attach tw-XXXX` to drive the paused page interactively. See [playwright-tests.md](playwright-tests.md) for the debug/attach mechanics and [test-generation.md](test-generation.md) for how every `playwright-cli` action emits Playwright TypeScript. + +--- + +## 1. Planning + +Goal: produce a spec file (e.g. `specs/.plan.md`) that enumerates the scenarios to test. **Always** write the spec to a file. + +### 1.1 Prerequisite: workspace + +Check the workspace has Playwright installed before anything else: + +```bash +# Either of these confirms a workspace: +test -f playwright.config.ts || test -f playwright.config.js +npx --no-install playwright --version +``` + +If there is no Playwright install, bootstrap one and let the user pick the defaults: + +```bash +npm init playwright@latest +``` + +### 1.2 Prerequisite: seed test + +A **seed test** is a minimal test that lands the page in the state every scenario starts from: navigation to the app, any required login, feature flags, etc. Scenarios assume a fresh start _after_ the seed. `--debug=cli` pauses _inside_ this test, so the seed is where every planning and generation session begins. + +Minimum viable seed: + +```ts +// tests/seed.spec.ts +import { test } from "@playwright/test"; + +test("seed", async ({ page }) => { + await page.goto("https://example.com/"); +}); +``` + +Preferred — push navigation into a fixture so scenario tests reuse it: + +```ts +// tests/fixtures.ts +import { test as baseTest } from "@playwright/test"; +export { expect } from "@playwright/test"; + +export const test = baseTest.extend({ + page: async ({ page }, use) => { + await page.goto("https://example.com/"); + await use(page); + }, +}); +``` + +```ts +// tests/seed.spec.ts +import { test } from "./fixtures"; + +test("seed", async ({ page }) => { + // Fixture already navigates. This empty body tells agents where to start. +}); +``` + +If no seed exists, create one that at least navigates to the app. + +### 1.3 Explore the app + +Launch the app via the seed in the background and attach: + +```bash +PLAYWRIGHT_HTML_OPEN=never npx playwright test tests/seed.spec.ts --debug=cli +# wait for "Debugging Instructions" and the session name tw-XXXX +playwright-cli attach tw-XXXX +``` + +Resume so the seed runs, then probe the app: + +```bash +playwright-cli resume # resume so that seed test runs fully +playwright-cli snapshot # inventory of interactive elements +playwright-cli click e5 # follow a flow +playwright-cli eval "location.href" # read URL / state +playwright-cli show --annotate # ask the user to point at something +``` + +Map out: + +- Interactive surfaces (forms, buttons, lists, filters, modals). +- Primary user journeys end-to-end. +- Edge cases: empty states, validation errors, very long input, boundary values. +- Persistence: reload, local/session storage, URL fragments. +- Navigation: which controls change the URL, back/forward behaviour. + +**Important**: Do not just open the app url with playwright-cli, always go through the test to capture any custom setup done there. +**Important**: Stop the background test when done exploring. + +### 1.4 Write the spec file + +Save under `specs/.plan.md`. Use this structure: + +```markdown +# Test Plan + +## Application Overview + + + +## Test Scenarios + +### 1. + +**Seed:** `tests/seed.spec.ts` + +#### 1.1. + +**File:** `tests//.spec.ts` + +**Steps:** + +1. + - expect: + - expect: + +2. + - expect: + +#### 1.2. + +... + +### 2. + +**Seed:** `tests/seed.spec.ts` +... +``` + +Guidelines: + +- Each scenario is independent and starts from the seed's fresh state — never chain scenarios. +- Scenario names are kebab-case and match the test file name (`should-add-single-todo` → `should-add-single-todo.spec.ts`). +- Cover happy path, edge cases, validation, negative flows, persistence. +- Write steps at the user level ("Type 'Buy milk' into the input"), not the API level ("call `fill`"). +- Put observable outcomes in `- expect:` bullets; each becomes an assertion during generation. + +--- + +## 2. Generate + +Goal: take a spec file and produce Playwright test files. Optionally update the spec if it has drifted. + +### 2.1 Inputs + +- **Spec file**, e.g. `specs/basic-operations.plan.md`. +- **Target**: either a single scenario (e.g. `1.2`), a whole group (`1`), or all. +- **Seed file**, read from the `**Seed:**` line of the scenario's group. + +### 2.2 Generate one scenario + +For each target scenario, in sequence (never in parallel — scenarios share the seed session): + +```bash +PLAYWRIGHT_HTML_OPEN=never npx playwright test --debug=cli # background +playwright-cli attach tw-XXXX +# resume +``` + +**Do not** just open the app url with playwright-cli, always go through the test to capture any custom setup done there. + +Walk the scenario's `Steps:` one by one with `playwright-cli`, treating the spec as the plan and the live app as the source of truth. If a step is vague ("click the button" — which button?), references an element that no longer exists, or contradicts the app's actual behaviour, use your judgement: update the spec to match what the app really does, then keep going. Editing the spec mid-generation is expected. + +Every action prints the equivalent Playwright TypeScript (see [test-generation.md](test-generation.md)): + +```bash +playwright-cli snapshot # find refs +playwright-cli fill e3 "John Doe" # -> page.getByRole('textbox', {...}).fill(...) +playwright-cli press Enter +playwright-cli click e7 +``` + +For each `- expect:` bullet, add an explicit assertion. See [test-generation.md](test-generation.md) for details. + +Collect the generated code and write the test file at the path given in the spec: + +```ts +// spec: specs/basic-operations.plan.md +// seed: tests/seed.spec.ts +import { test, expect } from "./fixtures"; // or '@playwright/test' if no fixtures file + +test.describe("Singing in and out", () => { + test("should sign in", async ({ page }) => { + // 1. Navigate to the application + // (handled by the seed fixture) + + // 2. Type 'John Doe' into the username field + await page.getByRole("textbox", { name: "username" }).fill("John Doe"); + + // 3. Type password + await page.getByRole("textbox", { name: "password" }).fill("TestPassword"); + + // 4. Press Enter to submit + await page.getByRole("textbox", { name: "password" }).press("Enter"); + + await expect(page.getByRole("heading")).toContainText("Welcome, John Doe!"); + }); +}); +``` + +Rules: + +- **One test per file.** File path, describe name, and test name come verbatim from the spec (minus the ordinal). +- Prefix each numbered step with a `// N. ` comment before its actions. +- Use the describe group name verbatim from the spec (no `1.` ordinal). +- Import from `./fixtures` if the project has one; otherwise `@playwright/test`. +- **Important**: close the CLI session and stop the background test before moving to the next scenario. + +### 2.3 Generate multiple scenarios + +Loop 2.2 over the targeted scenarios one at a time, restarting the seed between each so every test starts from a clean page. This is safe to parallelise due to unique generated session names - just make sure each test run is stopped. + +### 2.4 Run generated tests + +After generation, run the new tests once: + +```bash +PLAYWRIGHT_HTML_OPEN=never npx playwright test tests//.spec.ts +``` + +Any failure goes to Section 3. + +--- + +## 3. Heal + +Goal: fix failing tests, and update the spec if the app's intended behaviour changed. + +### 3.1 Find failing tests + +```bash +PLAYWRIGHT_HTML_OPEN=never npx playwright test +``` + +Record the list of failing `:` entries and process them one at a time. Do not attempt parallel fixes — shared state and the single CLI session make that fragile. + +### 3.2 Debug one failure + +Run the single failing test in debug mode in the background, then attach: + +```bash +PLAYWRIGHT_HTML_OPEN=never npx playwright test tests//.spec.ts: --debug=cli +# wait for "Debugging Instructions" and the tw-XXXX session name +playwright-cli attach tw-XXXX +``` + +The test is paused at the start. Step forward or run to until just before the failing action or assertion, then diagnose: + +```bash +playwright-cli snapshot # did the element change / move / rename? +playwright-cli console # app-side errors? +playwright-cli network # failed request? wrong payload? +playwright-cli show --annotate # ask the user to point somewhere +``` + +Common causes: selector drift, new wrapper element, label/ARIA rename, timing (transition, async load), assertion text updated in the app, test data leaking between runs. + +Rehearse the corrected interaction with `playwright-cli` — the generated code in the output is what you paste back into the test. + +### 3.3 Apply the fix + +Edit the test file: update the locator, assertion, step order, or inputs to match the corrected behaviour. Stop the background debug run. Rerun the single test to confirm green. + +Never skip hooks or add sleeps as a fix. Never use `networkidle`. + +### 3.4 Reconcile with the spec + +Open the spec referenced by the `// spec:` header in the test file and locate the scenario that matches the test. + +- **Fix was purely technical** (locator drift, better assertion shape) and the spec's user-level behaviour still matches the app → leave the spec alone. +- **Fix changed user-visible steps, inputs, order, or expected outcomes** that the spec describes → update the spec to match reality. Keep the scenario id and file path stable; only the step / expect lines change. +- **Unclear whether the app change is intentional** (spec is stale) **or a regression** (test was right, app is wrong) → **stop and ask the user**. Provide: + - the scenario id (e.g. `2.3`), + - the spec lines that no longer match, + - the observed app behaviour (quote a snapshot excerpt or a concrete outcome). + +Only after the user answers, either update the spec (intentional change) or file/flag the test as covering a bug (regression). + +### 3.5 Iteration and giving up + +- Fix failures one at a time; rerun after each. +- If after thorough investigation you are confident the test is correct but the app is wrong _and_ the user has confirmed it's a bug: mark the test `test.fixme(...)` with a comment pointing at the user's decision or issue link. Never silently skip. + +--- + +## Cross-references + +| For... | See | +| ---------------------------------------------- | ---------------------------------------------- | +| `--debug=cli` / attach mechanics | [playwright-tests.md](playwright-tests.md) | +| How `playwright-cli` actions become TS | [test-generation.md](test-generation.md) | +| Mocking requests during exploration/generation | [request-mocking.md](request-mocking.md) | +| Managing the CLI browser session | [session-management.md](session-management.md) | diff --git a/.claude/skills/playwright-cli/references/storage-state.md b/.claude/skills/playwright-cli/references/storage-state.md new file mode 100644 index 0000000000..c856db5e40 --- /dev/null +++ b/.claude/skills/playwright-cli/references/storage-state.md @@ -0,0 +1,275 @@ +# Storage Management + +Manage cookies, localStorage, sessionStorage, and browser storage state. + +## Storage State + +Save and restore complete browser state including cookies and storage. + +### Save Storage State + +```bash +# Save to auto-generated filename (storage-state-{timestamp}.json) +playwright-cli state-save + +# Save to specific filename +playwright-cli state-save my-auth-state.json +``` + +### Restore Storage State + +```bash +# Load storage state from file +playwright-cli state-load my-auth-state.json + +# Reload page to apply cookies +playwright-cli open https://example.com +``` + +### Storage State File Format + +The saved file contains: + +```json +{ + "cookies": [ + { + "name": "session_id", + "value": "abc123", + "domain": "example.com", + "path": "/", + "expires": 1735689600, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "https://example.com", + "localStorage": [ + { "name": "theme", "value": "dark" }, + { "name": "user_id", "value": "12345" } + ] + } + ] +} +``` + +## Cookies + +### List All Cookies + +```bash +playwright-cli cookie-list +``` + +### Filter Cookies by Domain + +```bash +playwright-cli cookie-list --domain=example.com +``` + +### Filter Cookies by Path + +```bash +playwright-cli cookie-list --path=/api +``` + +### Get Specific Cookie + +```bash +playwright-cli cookie-get session_id +``` + +### Set a Cookie + +```bash +# Basic cookie +playwright-cli cookie-set session abc123 + +# Cookie with options +playwright-cli cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax + +# Cookie with expiration (Unix timestamp) +playwright-cli cookie-set remember_me token123 --expires=1735689600 +``` + +### Delete a Cookie + +```bash +playwright-cli cookie-delete session_id +``` + +### Clear All Cookies + +```bash +playwright-cli cookie-clear +``` + +### Advanced: Multiple Cookies or Custom Options + +For complex scenarios like adding multiple cookies at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.context().addCookies([ + { name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true }, + { name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' } + ]); +}" +``` + +## Local Storage + +### List All localStorage Items + +```bash +playwright-cli localstorage-list +``` + +### Get Single Value + +```bash +playwright-cli localstorage-get token +``` + +### Set Value + +```bash +playwright-cli localstorage-set theme dark +``` + +### Set JSON Value + +```bash +playwright-cli localstorage-set user_settings '{"theme":"dark","language":"en"}' +``` + +### Delete Single Item + +```bash +playwright-cli localstorage-delete token +``` + +### Clear All localStorage + +```bash +playwright-cli localstorage-clear +``` + +### Advanced: Multiple Operations + +For complex scenarios like setting multiple values at once, use `run-code`: + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + localStorage.setItem('token', 'jwt_abc123'); + localStorage.setItem('user_id', '12345'); + localStorage.setItem('expires_at', Date.now() + 3600000); + }); +}" +``` + +## Session Storage + +### List All sessionStorage Items + +```bash +playwright-cli sessionstorage-list +``` + +### Get Single Value + +```bash +playwright-cli sessionstorage-get form_data +``` + +### Set Value + +```bash +playwright-cli sessionstorage-set step 3 +``` + +### Delete Single Item + +```bash +playwright-cli sessionstorage-delete step +``` + +### Clear sessionStorage + +```bash +playwright-cli sessionstorage-clear +``` + +## IndexedDB + +### List Databases + +```bash +playwright-cli run-code "async page => { + return await page.evaluate(async () => { + const databases = await indexedDB.databases(); + return databases; + }); +}" +``` + +### Delete Database + +```bash +playwright-cli run-code "async page => { + await page.evaluate(() => { + indexedDB.deleteDatabase('myDatabase'); + }); +}" +``` + +## Common Patterns + +### Authentication State Reuse + +```bash +# Step 1: Login and save state +playwright-cli open https://app.example.com/login +playwright-cli snapshot +playwright-cli fill e1 "user@example.com" +playwright-cli fill e2 "password123" +playwright-cli click e3 + +# Save the authenticated state +playwright-cli state-save auth.json + +# Step 2: Later, restore state and skip login +playwright-cli state-load auth.json +playwright-cli open https://app.example.com/dashboard +# Already logged in! +``` + +### Save and Restore Roundtrip + +```bash +# Set up authentication state +playwright-cli open https://example.com +playwright-cli eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }" + +# Save state to file +playwright-cli state-save my-session.json + +# ... later, in a new session ... + +# Restore state +playwright-cli state-load my-session.json +playwright-cli open https://example.com +# Cookies and localStorage are restored! +``` + +## Security Notes + +- Never commit storage state files containing auth tokens +- Add `*.auth-state.json` to `.gitignore` +- Delete state files after automation completes +- Use environment variables for sensitive data +- By default, sessions run in-memory mode which is safer for sensitive operations diff --git a/.claude/skills/playwright-cli/references/test-generation.md b/.claude/skills/playwright-cli/references/test-generation.md new file mode 100644 index 0000000000..9ce1fe5ad2 --- /dev/null +++ b/.claude/skills/playwright-cli/references/test-generation.md @@ -0,0 +1,138 @@ +# Test Generation + +Generate Playwright test code automatically as you interact with the browser. + +## How It Works + +Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. +This code appears in the output and can be copied directly into your test files. + +## Example Workflow + +```bash +# Start a session +playwright-cli open https://example.com/login + +# Take a snapshot to see elements +playwright-cli snapshot +# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"] + +# Fill form fields - generates code automatically +playwright-cli fill e1 "user@example.com" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); + +playwright-cli fill e2 "password123" +# Ran Playwright code: +# await page.getByRole('textbox', { name: 'Password' }).fill('password123'); + +playwright-cli click e3 +# Ran Playwright code: +# await page.getByRole('button', { name: 'Sign In' }).click(); +``` + +## Building a Test File + +Collect the generated code into a Playwright test: + +```typescript +import { test, expect } from "@playwright/test"; + +test("login flow", async ({ page }) => { + // Generated code from playwright-cli session: + await page.goto("https://example.com/login"); + await page.getByRole("textbox", { name: "Email" }).fill("user@example.com"); + await page.getByRole("textbox", { name: "Password" }).fill("password123"); + await page.getByRole("button", { name: "Sign In" }).click(); + + // Add assertions + await expect(page).toHaveURL(/.*dashboard/); +}); +``` + +## Best Practices + +### 1. Use Semantic Locators + +The generated code uses role-based locators when possible, which are more resilient: + +```typescript +// Generated (good - semantic) +await page.getByRole("button", { name: "Submit" }).click(); + +// Avoid (fragile - CSS selectors) +await page.locator("#submit-btn").click(); +``` + +### 2. Explore Before Recording + +Take snapshots to understand the page structure before recording actions: + +```bash +playwright-cli open https://example.com +playwright-cli snapshot +# Review the element structure +playwright-cli click e5 +``` + +### 3. Add Assertions Manually + +Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers: + +- `toBeVisible()` — element is rendered and visible +- `toHaveText(text)` — element text content matches +- `toHaveValue(value) / toBeEmpty()` — input/select value matches +- `toBeChecked() / toBeUnchecked()` — checkbox state matches +- `toMatchAriaSnapshot(snapshot)` — page (or locator) matches a partial accessibility snapshot + +Use `playwright-cli generate-locator ` to produce the locator expression for the assertion, and the snapshot/eval commands to capture the expected value. + +When asserting text content, make sure that generated locator does not contain text from the element itself. `getByTestId()` or `getByLabel()` usually work well with asserting text. When locator is text-based, prefer `toBeVisible()` instead. + +Snapshot to be matched does not have to contain all the information - only capture what's necessary for the assertion. You can use regular expressions for unstable values. + +```bash +# Get a stable locator for an element ref to use in the assertion +playwright-cli --raw generate-locator e5 +# getByRole('button', { name: 'Submit' }) + +# Capture expected text content for toHaveText +playwright-cli --raw eval "el => el.textContent" e5 + +# Capture expected input value for toHaveValue/toBeEmpty +playwright-cli --raw eval "el => el.value" e5 + +# Capture expected aria snapshot for toMatchAriaSnapshot/toBeChecked +# (whole page, or use a ref to scope to a region) +playwright-cli --raw snapshot +playwright-cli --raw snapshot e5 +``` + +```typescript +// Generated action +await page.getByRole("button", { name: "Submit" }).click(); + +// Manual assertions using the outputs above: +await expect(page.getByRole("alert", { name: "Success" })).toBeVisible(); +await expect(page.getByTestId("main-header")).toHaveText("Welcome, user"); +await expect(page.getByRole("textbox", { name: "Email" })).toHaveValue( + "user@example.com", +); +await expect( + page.getByRole("checkbox", { name: "Enable notifications" }), +).toBeChecked(); + +// toMatchAriaSnapshot on the whole page, finds a matching region +await expect(page).toMatchAriaSnapshot(` + - heading "Welcome, user" + - link /\\d+ new messages?/ + - button "Sign out" +`); + +// toMatchAriaSnapshot scoped to a region +await expect(page.getByRole("navigation")).toMatchAriaSnapshot(` + - link "Home" + - link /\\d+ new messages?/ + - link "Profile" +`); +``` diff --git a/.claude/skills/playwright-cli/references/tracing.md b/.claude/skills/playwright-cli/references/tracing.md new file mode 100644 index 0000000000..d81cdeb12b --- /dev/null +++ b/.claude/skills/playwright-cli/references/tracing.md @@ -0,0 +1,142 @@ +# Tracing + +Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs. + +## Basic Usage + +```bash +# Start trace recording +playwright-cli tracing-start + +# Perform actions +playwright-cli open https://example.com +playwright-cli click e1 +playwright-cli fill e2 "test" + +# Stop trace recording +playwright-cli tracing-stop +``` + +## Trace Output Files + +When you start tracing, Playwright creates a `traces/` directory with several files: + +### `trace-{timestamp}.trace` + +**Action log** - The main trace file containing: + +- Every action performed (clicks, fills, navigations) +- DOM snapshots before and after each action +- Screenshots at each step +- Timing information +- Console messages +- Source locations + +### `trace-{timestamp}.network` + +**Network log** - Complete network activity: + +- All HTTP requests and responses +- Request headers and bodies +- Response headers and bodies +- Timing (DNS, connect, TLS, TTFB, download) +- Resource sizes +- Failed requests and errors + +### `resources/` + +**Resources directory** - Cached resources: + +- Images, fonts, stylesheets, scripts +- Response bodies for replay +- Assets needed to reconstruct page state + +## What Traces Capture + +| Category | Details | +| --------------- | -------------------------------------------------- | +| **Actions** | Clicks, fills, hovers, keyboard input, navigations | +| **DOM** | Full DOM snapshot before/after each action | +| **Screenshots** | Visual state at each step | +| **Network** | All requests, responses, headers, bodies, timing | +| **Console** | All console.log, warn, error messages | +| **Timing** | Precise timing for each operation | + +## Use Cases + +### Debugging Failed Actions + +```bash +playwright-cli tracing-start +playwright-cli open https://app.example.com + +# This click fails - why? +playwright-cli click e5 + +playwright-cli tracing-stop +# Open trace to see DOM state when click was attempted +``` + +### Analyzing Performance + +```bash +playwright-cli tracing-start +playwright-cli open https://slow-site.com +playwright-cli tracing-stop + +# View network waterfall to identify slow resources +``` + +### Capturing Evidence + +```bash +# Record a complete user flow for documentation +playwright-cli tracing-start + +playwright-cli open https://app.example.com/checkout +playwright-cli fill e1 "4111111111111111" +playwright-cli fill e2 "12/25" +playwright-cli fill e3 "123" +playwright-cli click e4 + +playwright-cli tracing-stop +# Trace shows exact sequence of events +``` + +## Trace vs Video vs Screenshot + +| Feature | Trace | Video | Screenshot | +| ----------------------- | ----------- | ----------- | ---------------- | +| **Format** | .trace file | .webm video | .png/.jpeg image | +| **DOM inspection** | Yes | No | No | +| **Network details** | Yes | No | No | +| **Step-by-step replay** | Yes | Continuous | Single frame | +| **File size** | Medium | Large | Small | +| **Best for** | Debugging | Demos | Quick capture | + +## Best Practices + +### 1. Start Tracing Before the Problem + +```bash +# Trace the entire flow, not just the failing step +playwright-cli tracing-start +playwright-cli open https://example.com +# ... all steps leading to the issue ... +playwright-cli tracing-stop +``` + +### 2. Clean Up Old Traces + +Traces can consume significant disk space: + +```bash +# Remove traces older than 7 days +find .playwright-cli/traces -mtime +7 -delete +``` + +## Limitations + +- Traces add overhead to automation +- Large traces can consume significant disk space +- Some dynamic content may not replay perfectly diff --git a/.claude/skills/playwright-cli/references/video-recording.md b/.claude/skills/playwright-cli/references/video-recording.md new file mode 100644 index 0000000000..dd1f9e8bce --- /dev/null +++ b/.claude/skills/playwright-cli/references/video-recording.md @@ -0,0 +1,157 @@ +# Video Recording + +Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec). + +## Basic Recording + +```bash +# Open browser first +playwright-cli open + +# Start recording +playwright-cli video-start demo.webm + +# Add a chapter marker for section transitions +playwright-cli video-chapter "Getting Started" --description="Opening the homepage" --duration=2000 + +# Navigate and perform actions +playwright-cli goto https://example.com +playwright-cli snapshot +playwright-cli click e1 + +# Add another chapter +playwright-cli video-chapter "Filling Form" --description="Entering test data" --duration=2000 +playwright-cli fill e2 "test input" + +# Stop and save +playwright-cli video-stop +``` + +## Best Practices + +### 1. Use Descriptive Filenames + +```bash +# Include context in filename +playwright-cli video-start recordings/login-flow-2024-01-15.webm +playwright-cli video-start recordings/checkout-test-run-42.webm +``` + +### 2. Record entire hero scripts. + +When recording a video for the user or as a proof of work, it is best to create a code snippet and execute it with run-code. +It allows pulling appropriate pauses between the actions and annotating the video. There are new Playwright APIs for that. + +1. Perform scenario using CLI and take note of all locators and actions. You'll need those locators to request their bounding boxes for highlight. +2. Create a file with the intended script for video (below). Use pressSequentially w/ delay for nice typing, make reasonable pauses. +3. Use playwright-cli run-code --filename your-script.js + +**Important**: Overlays are `pointer-events: none` — they do not interfere with page interactions. You can safely keep sticky overlays visible while clicking, filling, or performing any actions on the page. + +```js +async (page) => { + await page.screencast.start({ + path: "video.webm", + size: { width: 1280, height: 800 }, + }); + await page.goto("https://demo.playwright.dev/todomvc"); + + // Show a chapter card — blurs the page and shows a dialog. + // Blocks until duration expires, then auto-removes. + // Use this for simple use cases, but always feel free to hand-craft your own beautiful + // overlay via await page.screencast.showOverlay(). + await page.screencast.showChapter("Adding Todo Items", { + description: "We will add several items to the todo list.", + duration: 2000, + }); + + // Perform action + await page + .getByRole("textbox", { name: "What needs to be done?" }) + .pressSequentially("Walk the dog", { delay: 60 }); + await page + .getByRole("textbox", { name: "What needs to be done?" }) + .press("Enter"); + await page.waitForTimeout(1000); + + // Show next chapter + await page.screencast.showChapter("Verifying Results", { + description: "Checking the item appeared in the list.", + duration: 2000, + }); + + // Add a sticky annotation that stays while you perform actions. + // Overlays are pointer-events: none, so they won't block clicks. + const annotation = await page.screencast.showOverlay(` +
+ ✓ Item added successfully +
+ `); + + // Perform more actions while the annotation is visible + await page + .getByRole("textbox", { name: "What needs to be done?" }) + .pressSequentially("Buy groceries", { delay: 60 }); + await page + .getByRole("textbox", { name: "What needs to be done?" }) + .press("Enter"); + await page.waitForTimeout(1500); + + // Remove the annotation when done + await annotation.dispose(); + + // You can also highlight relevant locators and provide contextual annotations. + const bounds = await page.getByText("Walk the dog").boundingBox(); + await page.screencast.showOverlay( + ` +
+
+
Check it out, it is right above this text +
+ `, + { duration: 2000 }, + ); + + await page.screencast.stop(); +}; +``` + +Embrace creativity, overlays are powerful. + +### Overlay API Summary + +| Method | Use Case | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| `page.screencast.showChapter(title, { description?, duration?, styleSheet? })` | Full-screen chapter card with blurred backdrop — ideal for section transitions | +| `page.screencast.showOverlay(html, { duration? })` | Custom HTML overlay — use for callouts, labels, highlights | +| `disposable.dispose()` | Remove a sticky overlay added without duration | +| `page.screencast.hideOverlays()` / `page.screencast.showOverlays()` | Temporarily hide/show all overlays | + +## Tracing vs Video + +| Feature | Video | Tracing | +| -------- | -------------------- | ---------------------------------------- | +| Output | WebM file | Trace file (viewable in Trace Viewer) | +| Shows | Visual recording | DOM snapshots, network, console, actions | +| Use case | Demos, documentation | Debugging, analysis | +| Size | Larger | Smaller | + +## Limitations + +- Recording adds slight overhead to automation +- Large recordings can consume significant disk space diff --git a/.claude/skills/testing-skill/SKILL.md b/.claude/skills/testing-skill/SKILL.md new file mode 100644 index 0000000000..ab70133c11 --- /dev/null +++ b/.claude/skills/testing-skill/SKILL.md @@ -0,0 +1,62 @@ +--- +name: testing-skill +description: Instructions for writing, running, and updating unit/end-to-end tests. Should be used when prompted specifically to add tests for a given feature, bug, or regression. +--- + +# Testing + +In most cases, once a feature, bug fix, or other modification has been written, it will need to have tests added, or existing tests updated. + +## Test File Locations + +### Unit Tests + +`/tests/src/unit`: Contains the bulk of unit tests, mainly relating to interoperability between BlockNote's JSON format and HTML/Markdown. Also includes some miscellaneous tests, like React rendering, selection handling, and NextJS integration. + +`/packages/core/src/api`: Contains mainly tests for getting, inserting, updating, and removing blocks, etc, under `/blockManipulation/commands`. Also includes tests for intermediary functionality between BlockNote and the underlying TipTap editor, like converting between blocks & nodes, or setting editor event handlers. + +`/packages/xl-*`: Contain tests for functionality included in a given `xl-*` package. + +### End-to-End Tests + +`tests/src/end-to-end`: Any test which interacts with the editor UI or simulates user interaction goes here. New subdirectories can be added if the functionality being tested is not covered by any of the existing ones. Important note about existing E2E tests - many are written poorly and should only loosely be used as reference. We want to avoid abstraction layers and `waitForTimeout` as much as possible. + +## When & How to Add Tests + +In general, we expect a change in code to result in failing test cases. If this does not happen, tests should be added and checked to ensure they pass with the code changes while failing without them. + +However, this may not be true when adding edge case handling or a new feature, where existing tests may all continue to pass. In this case, tests should be added as necessary to cover all of the new functionality. We should still ensure that the new tests pass with the new code changes while failing without them. + +We want to avoid adding end-to-end tests where it's possible to use unit tests instead. + +## Running & Updating Tests + +### Unit Tests + +Unit tests can be run from the root directory using `vp run test`, which will run all of them across all directories. A specific test file may be targeted by appending its name, i.e. `vp run test fileName`. Individual tests in a file may be disabled using `skip`, i.e. `it.skip("Test name", ...)` (remember to revert this once all tests pass). + +Updating tests can be done by adding the `-u` argument, i.e. `vp run test -u`. All of the other things you can do to scope which tests to target still apply. + +### End-to-End Tests + +End-to-end tests run inside a docker container. While its possible to run them outside of it, we do not have existing snapshots to compare results with, and the results sometimes differ to when they're run within Docker, so it's not worth doing. + +To run end-to-end tests, you must first build the project and run the preview. You can do this by running `vp start` from the root directory. + +You can then run the tests from the `/tests` directory using the following command: + +``` +docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.51.1-noble npx playwright test +``` + +A specific test file may be targeted by appending its name, i.e. `... npx playwright test fileName`. Individual tests in a file may be disabled using `skip`, i.e. `test.skip("Test name", ...)` (remember to revert this once all tests pass). + +Updating tests can be done by adding the `-u` argument, i.e. `... npx playwright test -u`. All of the other things you can do to scope which tests to target still apply. + +Note that running this command may result in errors or other issues, listed below along with what to do when encountered: + +- **Tests failing to navigating to preview**: project should be built and the preview started, after which the command should be run again. +- **Docker not running**: the user should be notified to launch Docker. +- **Incorrect Playwright image version**: update Playwright images and re-run the command. + +When testing a visual change, prefer writing screenshots to verify that the change is working as expected. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..87697e5b29 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# Keep the e2e image build context lean and Linux-clean: never copy host +# (macOS) node_modules or build outputs — deps are installed fresh inside the +# image. What actually lands in the image is selected explicitly by the COPY +# steps in tests/Dockerfile (manifests + example apps), not by exclusions here; +# these entries just stop the heavy/irrelevant trees from bloating the context. +**/node_modules +**/dist +**/types +**/.vite +**/.vite-plus +**/*.tsbuildinfo +**/.DS_Store +.git +**/test-results +**/blob-report +**/playwright-report +tests/.vitest-attachments diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index e9511bbf61..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "react-app", - "react-app/jest" - ], - "parser": "@typescript-eslint/parser", - "plugins": ["import", "@typescript-eslint"], - "settings": { - "import/extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx"], - "import/external-module-folders": ["node_modules", "node_modules/@types"], - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".cts", ".mts", ".tsx"] - }, - "import/resolver": { - "node": { - "extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx"] - } - } - }, - "ignorePatterns": ["**/ui/*"], - "rules": { - "no-console": "error", - "curly": 1, - "import/extensions": ["error", "always", { "ignorePackages": true }], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "peerDependencies": true, - "optionalDependencies": false, - "bundledDependencies": false - } - ], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", - "import/no-cycle": "error" - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 18e7728a37..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** - - -**To Reproduce** - - -**Misc** - -- Node version: -- Package manager: -- Browser: -- [ ] I'm a [sponsor](https://www.blocknotejs.org/pricing) and would appreciate if you could look into this sooner than later 💖 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..372e429b34 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,101 @@ +name: Bug report +description: Report a bug or broken behavior in BlockNote +labels: + - bug + - needs-triage +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! + Please use this template to describe **broken or incorrect behavior**. + + Feature ideas or questions should go to **Discussions**. + + - type: textarea + id: problem + attributes: + label: What’s broken? + description: > + Describe the problem clearly and concisely. + What is happening that should not be happening? + placeholder: > + Example: + When editing a table cell and pressing Enter, the editor crashes and the document cannot be recovered. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + description: > + Describe the expected or correct behavior. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: > + Provide clear steps so we can reproduce the issue. + If possible, an online reproduction (e.g. StackBlitz) is extremely helpful. + placeholder: | + 1. Create a new document + 2. Insert a table + 3. Click inside a cell + 4. Press Enter + + Optional: If you can, provide a minimal online reproduction. + You can use this starter sandbox: + https://stackblitz.com/github/TypeCellOS/BlockNote/tree/main/examples/01-basic/01-minimal?file=App.tsx + validations: + required: false + + - type: input + id: version + attributes: + label: BlockNote version + description: > + Optional — specify the version you’re using, if known. + placeholder: e.g. v0.18.2 + validations: + required: false + + - type: input + id: environment + attributes: + label: Environment + description: > + Browser, OS, framework, or other relevant environment details. + placeholder: e.g. Chrome 121, macOS 14, React 18 + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional context + description: > + Screenshots, videos, logs, or any other context that might help. + validations: + required: false + + - type: checkboxes + id: contribute + attributes: + label: Contribution + options: + - label: "I'd be interested in contributing a fix for this issue" + required: false + + - type: checkboxes + id: sponsor + attributes: + label: Sponsor + description: > + Optional — helps us prioritize first response according to our SLA. + options: + - label: "I'm a [sponsor](https://www.blocknotejs.org/pricing) and would appreciate if you could look into this sooner than later 💖" + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..d431145bc6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false + +contact_links: + - name: Share an idea or suggest an enhancement + url: https://github.com/TypeCellOS/BlockNote/discussions/categories/ideas-enhancements + about: Share feature ideas, enhancement suggestions, or other ideas for the BlockNote project. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index e8dc8c2e5b..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. - -**Bonus** -[ ] I'm a [sponsor](https://www.blocknotejs.org/pricing) and would appreciate if you could look into this sooner than later 💖 diff --git a/.github/ISSUE_TEMPLATE/share_block.yml b/.github/ISSUE_TEMPLATE/share_block.yml deleted file mode 100644 index 889bb404be..0000000000 --- a/.github/ISSUE_TEMPLATE/share_block.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: "Submit a Custom Block" -description: "Use this template to submit a new custom block (schema) for BlockNote." -title: "" -body: - - type: markdown - attributes: - value: | - ## Submit a Custom Block (Schema) - - Thank you for contributing to BlockNote! Please fill out the following sections to submit your custom block. Make sure to provide as much detail as possible to help us understand and showcase your block effectively. - - - type: input - id: block_name - attributes: - label: "Block Name" - description: "Enter the name of your custom block." - placeholder: "e.g., Alert Block" - - - type: textarea - id: block_description - attributes: - label: "Block Description" - description: "Provide a detailed description of your custom block. Explain its purpose and how it works." - placeholder: "This block is used to display alert messages with different severity levels." - - - type: textarea - id: block_dependencies - attributes: - label: "Dependencies" - description: "Provide a full list of all the dependencies needed to make your custom block work." - placeholder: "`npm_package_1`, `npm_package_2`, ...etc " - - - type: input - id: live_example - attributes: - label: "Live Example" - description: "Please provide a valid URL to a codepen, stackblitz, codeSandbox, or simply a github repo" - placeholder: "live example URL here" - - - type: textarea - id: usage_instructions - attributes: - label: "Usage Instructions" - description: "Explain how to use your custom block in a BlockNote application. Include any necessary setup steps." - placeholder: | - ```markdown - 1. Import your custom block. - 2. Add it to the BlockNote editor configuration. - 3. Use it within the BlockNote editor. - ``` - - - type: textarea - id: additional_notes - attributes: - label: "Additional Notes" - description: "Include any additional information or notes about your custom block." - placeholder: "Any additional context or information." diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..4cd39c8b21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,68 @@ +version: 2 +updates: + - package-ecosystem: "npm" + versioning-strategy: "lockfile-only" + directories: + - "/" + - "/packages/*" + schedule: + interval: "weekly" + labels: + - "npm dependencies" + commit-message: + prefix: "chore" + allow: + # @tiptap packages + - dependency-name: "@tiptap/core" + - dependency-name: "@tiptap/pm" + - dependency-name: "@tiptap/react" + - dependency-name: "@tiptap/extensions" + - dependency-name: "@tiptap/extension-bold" + - dependency-name: "@tiptap/extension-code" + - dependency-name: "@tiptap/extension-horizontal-rule" + - dependency-name: "@tiptap/extension-italic" + + - dependency-name: "@tiptap/extension-paragraph" + - dependency-name: "@tiptap/extension-strike" + - dependency-name: "@tiptap/extension-text" + - dependency-name: "@tiptap/extension-underline" + # prosemirror packages + - dependency-name: "prosemirror-changeset" + - dependency-name: "prosemirror-highlight" + - dependency-name: "prosemirror-model" + - dependency-name: "prosemirror-state" + - dependency-name: "prosemirror-tables" + - dependency-name: "prosemirror-transform" + - dependency-name: "prosemirror-view" + # react packages + - dependency-name: "react" + - dependency-name: "react-dom" + # yjs packages + - dependency-name: "yjs" + - dependency-name: "y-prosemirror" + ignore: + # Hono packages are used only in the demo AI server and are not part of + # the main editor/runtime surface area. + - dependency-name: "hono" + - dependency-name: "@hono/node-server" + - dependency-name: "@hono/*" + groups: + editor-dependencies: + patterns: + - "@tiptap/*" + - "prosemirror-*" + - "react" + - "react-dom" + - "yjs" + - "y-prosemirror" + cooldown: + default-days: 7 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + include: "scope" + cooldown: + default-days: 7 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa013cc9a2..2f8f7dfc38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,9 +6,11 @@ on: pull_request: types: [opened, synchronize, reopened, edited] +permissions: + contents: read + env: - NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }} - NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build: @@ -16,106 +18,184 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 100 + persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - uses: nrwl/nx-set-shas@v3 - - - uses: actions/setup-node@v4 + - uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1 with: - cache: "pnpm" - cache-dependency-path: "**/pnpm-lock.yaml" - node-version-file: ".nvmrc" + node-version-file: ".node-version" + cache: true - - name: Cache NX - uses: actions/cache@v4 - with: - path: .nx/cache - key: nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }} - restore-keys: | - nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}- - nx-${{ env.NX_BRANCH }}- - nx- - - # This is needed for the canvas dep, Tiptap V3 should remove the need for this - - run: sudo apt-get update && sudo apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev pkg-config - name: Install Dependencies - run: pnpm install + run: vp install - name: Lint packages - run: pnpm run lint + run: vp lint - name: Build packages - run: pnpm run build + run: vp run -r build - name: Run unit tests - run: pnpm run test + run: vp run -r test + + - name: Run Next.js integration test (production build) + run: NEXTJS_TEST_MODE=build vp test run src/unit/nextjs/serverUtil.test.ts + working-directory: tests - name: Upload webpack stats artifact (editor) - uses: relative-ci/agent-upload-artifact-action@v2 + uses: relative-ci/agent-upload-artifact-action@a2b5741b4f7e6a989c84ec1a3059696b23c152e5 # v2 with: webpackStatsFile: ./playground/dist/webpack-stats.json artifactName: relative-ci-artifacts-editor - name: Soft release id: soft-release - run: pnpx pkg-pr-new publish './packages/*' # TODO disabled only for AI branch--compact - - playwright: - name: "Playwright Tests - ${{ matrix.browser }}" + run: vp dlx pkg-pr-new publish './packages/*' # TODO disabled only for AI branch--compact + + e2e: + # Vitest Browser Mode runs in the Playwright Linux container — the same + # environment as the local Docker run — so behaviour matches local dev. + # The suite resolves every `@blocknote/*` import to its `src/` via the + # aliases in `tests/vite.config.browser.ts` (vite transpiles the package + # sources on the fly), so the packages do NOT need to be built to `dist` + # first — `vp install` + the checked-out sources are enough. + name: "E2E - ${{ matrix.browser }} (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})" runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 30 container: - image: mcr.microsoft.com/playwright:v1.51.1-noble + image: mcr.microsoft.com/playwright:v1.60.0-noble strategy: fail-fast: false matrix: browser: [chromium, firefox, webkit] + shardIndex: [1, 2] + shardTotal: [2] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 100 + persist-credentials: false - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - uses: nrwl/nx-set-shas@v3 + - uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1 + with: + node-version-file: ".node-version" + cache: true - - uses: actions/setup-node@v4 + - name: Install dependencies + run: vp install + + # No preview server: Vitest Browser Mode serves the tests + mounted example + # apps itself. `--browser` selects this matrix job's browser and + # `--shard=/` splits that browser's test files across two + # parallel machines. Each shard records a machine-readable `blob` report + # (named per browser+shard so they don't collide); the `merge-reports` job + # stitches every browser's shards into one HTML report afterwards. + - name: Run e2e tests (${{ matrix.browser }} ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + run: HOME=/root vp test -c vite.config.browser.ts --run --browser ${{ matrix.browser }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=default --reporter=blob --outputFile.blob=blob-report/blob-${{ matrix.browser }}-${{ matrix.shardIndex }}.json + working-directory: tests + + - name: Upload blob report (${{ matrix.browser }} ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: ${{ !cancelled() }} with: - cache: "pnpm" - cache-dependency-path: "**/pnpm-lock.yaml" - node-version-file: ".nvmrc" + name: blob-report-${{ matrix.browser }}-${{ matrix.shardIndex }} + path: tests/blob-report/ + retention-days: 1 - - name: Cache NX - uses: actions/cache@v4 + - name: Upload failure artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: ${{ failure() }} + with: + name: e2e-attachments-${{ matrix.browser }}-${{ matrix.shardIndex }} + path: tests/.vitest-attachments/ + retention-days: 7 + + merge-reports: + # Stitch every browser+shard blob report into a single navigable HTML + # report (the Vitest equivalent of `playwright merge-reports`). Runs even + # when a shard failed, so the report always covers every browser. + name: "E2E Report" + runs-on: ubuntu-latest + needs: e2e + if: ${{ !cancelled() }} + timeout-minutes: 15 + # Same container as the e2e shards: `--mergeReports` doesn't run tests or + # launch browsers, but the HTML reporter still resolves the test files' + # browser environment, so the browser config must stay enabled (and present). + container: + image: mcr.microsoft.com/playwright:v1.60.0-noble + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: - path: .nx/cache - key: nx-${{ matrix.browser }}-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }} - restore-keys: | - nx-${{ matrix.browser }}-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}- - nx-${{ matrix.browser }}-${{ env.NX_BRANCH }}- - nx- + fetch-depth: 100 + persist-credentials: false - - run: apt-get update && apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev pkg-config + - uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1 + with: + node-version-file: ".node-version" + cache: true - name: Install dependencies - run: pnpm install + run: vp install - - name: Build packages - run: pnpm run build - - - name: Run server and Playwright tests - run: | - HOME=/root PLAYWRIGHT_CONFIG="--project ${{ matrix.browser }}" pnpm run e2e - - - uses: actions/upload-artifact@v4 - if: always() + # Gather every shard's blob into one directory (blob-report-chromium-1, …). + - name: Download blob reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + path: tests/blob-report + pattern: blob-report-* + merge-multiple: true + + # Re-emit a single HTML report from the merged blobs (no tests are run). + - name: Merge into HTML report + run: HOME=/root vp test -c vite.config.browser.ts --mergeReports=blob-report --reporter=html + working-directory: tests + + - name: Upload merged HTML report + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: ${{ !cancelled() }} with: - name: playwright-report-${{ matrix.browser }} + name: e2e-report path: tests/playwright-report/ - retention-days: 30 + retention-days: 7 + + deploy-report: + # Publish the merged report to a GitHub Pages PR preview and comment the + # link on the PR (Vercel-style). Pages serves it over HTTP, so the report's + # data loads correctly — unlike opening the downloaded artifact over file:// + # (the @vitest/ui report fetches its data and is blocked by CORS there). + # + # Default `needs` semantics: this runs only when `merge-reports` succeeded + # (i.e. a report exists) — but regardless of whether the tests passed, so a + # red run still gets a navigable preview. Skipped for fork PRs, whose + # read-only token can't push to gh-pages or comment (they keep the artifact). + name: "E2E Report Preview" + runs-on: ubuntu-latest + needs: merge-reports + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - name: Download merged report + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: e2e-report + path: e2e-report + + # Deploys to the `gh-pages` branch under `pr-preview/pr-/` and posts a + # sticky comment with the URL. The companion `pr-preview-cleanup` workflow + # removes it when the PR closes. + - name: Deploy report to PR preview + uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1 + with: + source-dir: e2e-report + preview-branch: gh-pages + umbrella-dir: pr-preview + action: deploy diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml new file mode 100644 index 0000000000..a1b441678d --- /dev/null +++ b/.github/workflows/fresh-install-tests.yml @@ -0,0 +1,148 @@ +name: Fresh Install Tests + +# Periodically tests BlockNote with the latest versions of its production +# dependencies (within declared semver ranges). This catches breakage when a +# new release of a dep like @tiptap/* or prosemirror-* ships and conflicts +# with BlockNote's declared ranges — the kind of failure a user would hit when +# running `npm install @blocknote/react` in a fresh project. +# +# Only production dependencies of published (non-private) packages are updated. +# DevDependencies (vitest, vite, typescript, etc.) stay pinned to the lockfile, +# so test tooling churn doesn't cause false positives. + +on: + schedule: + - cron: "0 2 * * *" # Daily at 02:00 UTC + workflow_dispatch: # Allow manual runs + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + fresh-install-unit-tests: + name: Unit Tests (Fresh Dep Resolution) + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - id: checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - id: setup_vp + uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1 + with: + node-version-file: ".node-version" + # Intentionally no install cache — we want fresh prod dep resolution. + cache: false + + - id: install_dependencies + name: Install dependencies + run: vp install + + - id: update_prod_deps + name: Update prod deps of published packages + # Resolves production dependencies of every published (non-private) + # workspace package to the latest version within their declared semver + # ranges. This simulates what a user gets when running + # `npm install @blocknote/react` in a fresh project. + # DevDependencies are left at their lockfile versions. + # NB: this uses pnpm directly because vp doesn't expose a `--prod`-only + # update flow; setup-vp installs pnpm on PATH so this still works. + run: | + FILTERS=$(node -e " + const fs = require('fs'); + const path = require('path'); + fs.readdirSync('packages').forEach(dir => { + try { + const pkg = JSON.parse(fs.readFileSync(path.join('packages', dir, 'package.json'), 'utf8')); + if (!pkg.private && pkg.name) process.stdout.write('--filter ' + pkg.name + ' '); + } catch {} + }); + ") + echo "Updating prod deps for: $FILTERS" + eval pnpm update --prod $FILTERS + + - id: dedupe_deps + name: Dedupe transitive dependencies + # After bumping the publishable packages' prod deps, collapse any + # duplicate transitive resolutions (e.g. @tiptap/core + @tiptap/pm) + # that would otherwise differ between the updated publishable packages + # and the un-updated examples/playground. Without this, TypeScript + # treats the two copies' exports as unrelated types and example-editor + # fails to build (TS2322 on Extension vs AnyExtension). + # Dedupe only rewrites the lockfile — it does NOT modify package.json, + # so the examples' "@blocknote/*": "latest" specs (which is what + # CodeSandbox users see) stay intact. + run: pnpm dedupe + + - id: build_packages + name: Build packages + run: vp run -r build + + - id: run_unit_tests + name: Run unit tests + run: vp run -r test + + - name: Notify Slack on workflow failure + if: ${{ failure() }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + REPOSITORY: ${{ github.repository }} + WORKFLOW: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + RUN_ATTEMPT: ${{ github.run_attempt }} + BRANCH: ${{ github.ref_name }} + STEPS_CHECKOUT_OUTCOME: ${{ steps.checkout.outcome }} + STEPS_SETUP_VP_OUTCOME: ${{ steps.setup_vp.outcome }} + STEPS_INSTALL_DEPENDENCIES_OUTCOME: ${{ steps.install_dependencies.outcome }} + STEPS_UPDATE_PROD_DEPS_OUTCOME: ${{ steps.update_prod_deps.outcome }} + STEPS_DEDUPE_DEPS_OUTCOME: ${{ steps.dedupe_deps.outcome }} + STEPS_BUILD_PACKAGES_OUTCOME: ${{ steps.build_packages.outcome }} + STEPS_RUN_UNIT_TESTS_OUTCOME: ${{ steps.run_unit_tests.outcome }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL is not configured; skipping Slack notification." + exit 0 + fi + + failed_step="Unknown step" + if [ "${STEPS_CHECKOUT_OUTCOME}" = "failure" ]; then + failed_step="Checkout repository" + elif [ "${STEPS_SETUP_VP_OUTCOME}" = "failure" ]; then + failed_step="Setup Vite+" + elif [ "${STEPS_INSTALL_DEPENDENCIES_OUTCOME}" = "failure" ]; then + failed_step="Install dependencies" + elif [ "${STEPS_UPDATE_PROD_DEPS_OUTCOME}" = "failure" ]; then + failed_step="Update prod deps of published packages" + elif [ "${STEPS_DEDUPE_DEPS_OUTCOME}" = "failure" ]; then + failed_step="Dedupe transitive dependencies" + elif [ "${STEPS_BUILD_PACKAGES_OUTCOME}" = "failure" ]; then + failed_step="Build packages" + elif [ "${STEPS_RUN_UNIT_TESTS_OUTCOME}" = "failure" ]; then + failed_step="Run unit tests" + fi + + run_url="https://github.com/${REPOSITORY}/actions/runs/${RUN_ID}" + message=$(printf '%s\n%s\n%s\n%s' \ + ":warning: Fresh Install Tests failed in *${REPOSITORY}* on branch *${BRANCH}*." \ + "*Workflow:* ${WORKFLOW}" \ + "*Run:* <${run_url}|#${RUN_NUMBER} (attempt ${RUN_ATTEMPT})>" \ + "*Failed step:* ${failed_step}") + payload=$(jq --compact-output --null-input --arg text "$message" '{text: $text}') + + curl -sS -X POST \ + --fail \ + --retry 4 \ + --retry-all-errors \ + --retry-max-time 60 \ + --connect-timeout 10 \ + --max-time 30 \ + -H "Content-type: application/json" \ + --data "$payload" \ + "$SLACK_WEBHOOK_URL" diff --git a/.github/workflows/pr-preview-cleanup.yml b/.github/workflows/pr-preview-cleanup.yml new file mode 100644 index 0000000000..239dc2f381 --- /dev/null +++ b/.github/workflows/pr-preview-cleanup.yml @@ -0,0 +1,30 @@ +name: pr-preview-cleanup + +# Removes the GitHub Pages e2e-report preview that `build.yml`'s `deploy-report` +# job published for a PR, once that PR is closed/merged. Kept separate from +# `build.yml` so closing a PR doesn't re-run the whole build + e2e suite. +on: + pull_request: + types: [closed] + +permissions: + contents: write + pull-requests: write + +jobs: + remove-preview: + name: "Remove E2E Report Preview" + runs-on: ubuntu-latest + # Fork PRs never got a preview (read-only token), so nothing to remove. + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - name: Remove PR preview + uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1 + with: + preview-branch: gh-pages + umbrella-dir: pr-preview + action: remove diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 875b54ff67..b39deb9957 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -5,58 +5,29 @@ on: push: tags: - v*.*.* + - v*.*.*-* + workflow_dispatch: + inputs: + version: + description: "Version tag to publish (e.g., v0.x.x-hotfix)" + required: true + type: string -env: - NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN }} - NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ secrets.NX_SELF_HOSTED_REMOTE_CACHE_SERVER }} +permissions: {} jobs: publish: name: Publish runs-on: ubuntu-latest - permissions: - contents: write - id-token: write # needed for provenance data generation - attestations: write - timeout-minutes: 10 + timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 100 - - run: jq '.packageManager' package.json | tr -d '"pnpm@' - id: package-manager-version - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: ${{ steps.package-manager-version.outputs.stdout }} - - - uses: nrwl/nx-set-shas@v3 - - - uses: actions/setup-node@v4 - with: - cache: "pnpm" - cache-dependency-path: "**/pnpm-lock.yaml" - node-version-file: ".nvmrc" - - - name: Cache NX - uses: actions/cache@v4 - with: - path: .nx/cache - key: nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}-${{ github.sha }} - restore-keys: | - nx-${{ env.NX_BRANCH }}-${{ env.NX_RUN_GROUP }}- - nx-${{ env.NX_BRANCH }}- - nx- - - - run: sudo apt-get update && sudo apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev pkg-config - - - name: Install Dependencies & Build - run: pnpm install && pnpm build - - - name: Print Environment Info - run: pnpm exec nx report - - - name: Publish packages - # Ensure npm 11.5.1 or later for trusted publishing - run: npm install -g npm@latest && pnpm exec nx release publish --access public + - name: Publish disabled + # NX has been removed (it was driving `nx release publish`) and the + # replacement release tool is not yet in place. This workflow is + # intentionally a no-op so a stray tag push doesn't silently succeed + # without actually publishing anything. Once a release tool is wired + # up, restore the publish steps and remove this guard. + run: | + echo "::error::Publish workflow is disabled — release tooling is being replaced." + echo "::error::Re-enable this workflow once a release tool is wired up." + exit 1 diff --git a/.github/workflows/relative-ci.yaml b/.github/workflows/relative-ci.yaml index c2b848e202..fdee2a57f5 100644 --- a/.github/workflows/relative-ci.yaml +++ b/.github/workflows/relative-ci.yaml @@ -1,17 +1,24 @@ name: RelativeCI on: + # zizmor: ignore[dangerous-triggers] -- workflow_run is the recommended pattern + # for RelativeCI; this workflow only downloads artifacts and reports bundle stats. workflow_run: workflows: ["build"] types: - completed +permissions: + actions: read + contents: read + jobs: build: runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' steps: - name: Send bundle stats and build information to RelativeCI (editor) - uses: relative-ci/agent-action@v2 + uses: relative-ci/agent-action@fcf45416581928e8dd62eded78ce98c78e5149f8 # v3.2.3 with: artifactName: relative-ci-artifacts-editor key: ${{ secrets.RELATIVE_CI_KEY }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..0b9ea34841 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,26 @@ +name: GitHub Actions Security Analysis with zizmor + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +permissions: {} + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 diff --git a/.gitignore b/.gitignore index 92f64ba492..a22a821267 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ examples/*/types tsconfig.tsbuildinfo # testing coverage +# Vitest's attachment store (failure screenshots / diffs). Accumulates across +# runs; the HTML reporter copies what it needs into playwright-report/data, +# so the originals are unreferenced cruft. +.vitest-attachments/ # production /build @@ -29,10 +33,18 @@ yarn-error.log* .vercel test-results/ playwright-report/ +blob-report/ release /test-results/ /playwright-report/ +/blob-report/ /playwright/.cache/ .env *.pem .nx/ +# Nightshift plan artifacts (keep out of version control) +.nightshift-plan +.claude/* +!.claude/skills +.playwright-cli +.claude/skills/playwright-cli/screenshots \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000..5bf4400f22 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.15.0 diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index c5ddcef4e7..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v22.14.0 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0f4e5b7d67..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/prettierrc", - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "printWidth": 80, - "trailingComma": "all", - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "plugins": ["prettier-plugin-tailwindcss"] -} diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100755 index 0000000000..85fb65b4fc --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c224675018..1b6c379e0a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,5 @@ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format - "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] + "recommendations": ["VoidZero.vite-plus-extension-pack"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c24081186..80f6014497 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,16 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true, - "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" }, + "oxc.fmt.configPath": "./vite.config.ts", + "npm.scriptRunner": "vp", + "[javascript]": { "editor.defaultFormatter": "oxc.oxc-vscode" }, + "[javascriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" }, + "[typescript]": { "editor.defaultFormatter": "oxc.oxc-vscode" }, + "[typescriptreact]": { "editor.defaultFormatter": "oxc.oxc-vscode" }, "search.exclude": { "packages/editor/public/types": true, "packages/website/docs/.vitepress": false, @@ -15,11 +21,5 @@ "editor.defaultFormatter": "redhat.vscode-xml" }, "scm.defaultViewMode": "tree", - "search.defaultViewMode": "tree", - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[mdx]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } + "search.defaultViewMode": "tree" } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..85d7918fe5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Project Description + +BlockNote is a block-based rich text editor for the web. It's designed as a batteries-included product that offers a solid user experience with minimal setup. However, it also offers extensibility via plugins and custom block types. + +# Issue Context + +When prompted to write a new feature, fix a bug, or make some other modification to the code, the project repository on GitHub should be scanned for issues and PRs which are relevant to the task at hand. Before writing any code, a summary of these should be given. If nothing relevant is found, the task can be started immediately. Otherwise, the user should be prompted on next steps. + +This should only be done for new conversations. If GitHub was already scanned in the same conversation, it does not need to be scanned again. + +Once the task is done and the feature is completed, bug is fixed, etc, the user should be reminded of the relevant issues and PRs found in the initial investigation. + +The GitHub CLI should be used to browse issues and PRs. + +# Common Commands + +All commands below are listed under `package.json` in the project root. See `vite.config.ts` for relevant configuration settings. + +- `vp install`: Installs dependencies. +- `vp run dev`: Starts the dev server on port 5173. +- `vp run check`: Checks for linting and formatting issues across the project and attempt resolve issues automatically. +- `vp run build`: Builds the project. +- `vp run preview`: Previews the build on port 3000. +- `vp run test`: Runs unit tests. Append with `-u` to update snapshots. Append with a file name to target only that file. +- `vp run e2e`: Runs end-to-end tests. Append with a file name to target only that file. +- `vp run e2e:updateSnaps`: Runs end-to-end tests & updates snapshots. Append with a file name to target only that file. +- `vp help`: Prints a list of all availabel commands. + +# Common Entry Points + +When writing a new feature, bug fix, or other modification, it may not be immediately clear where the code for it should be. There are a few files which are good to start looking in when this is the case: + +- `/packages/core/src/editor/BlockNoteEditor.ts`: Contains the class for the core BlockNote editor. Every editor command & event can be traced from here. +- `/packages/react/src/editor/BlockNoteView.tsx`: Contains the `BlockNoteViewEditor` component, which is the base for rendering the editor and its UI elements. Whenever the UI functionality (and often styling) needs to be changed, it will be a descendant of `BlockNoteViewEditor`. +- `/packages/mantine/src/BlockNoteView.tsx`: Contains the Mantine version of `BlockNoteView`. This can be thought of as a skin for `BlockNoteViewEditor` that uses the Mantine component library. Therefore, changes in `BlockNoteViewEditor` may also have to be propagted to it. + - The same applies for `BlockNoteView.tsx` in `/packages/ariakit` and `/packages/shadcn`, though Mantine is the defacto default version of `BlockNoteView`. + +# Additional Notes + +- Do not create git commits. diff --git a/CHANGELOG.md b/CHANGELOG.md index d67c41f199..87fed6062a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,494 @@ +## 0.51.4 (2026-06-02) + +### 🩹 Fixes + +- add explicit type annotations for callback params losing contextual typing ([#2815](https://github.com/TypeCellOS/BlockNote/pull/2815)) +- Comments emoji picker button issues (BLO-1199) ([#2769](https://github.com/TypeCellOS/BlockNote/pull/2769)) +- **core:** add editor cleanup in BlockNoteEditor.test.ts to prevent unhandled DOMObserver errors ([#2816](https://github.com/TypeCellOS/BlockNote/pull/2816)) +- **table:** prevent crash when pressing Enter in a table cell ([#2793](https://github.com/TypeCellOS/BlockNote/pull/2793)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- Nick Perez +- Nick the Sick @nperez0111 +- Yuki Terashima @y-temp4 + +## 0.51.3 (2026-05-26) + +### 🩹 Fixes + +- table cell colors (BLO-1198) ([#2770](https://github.com/TypeCellOS/BlockNote/pull/2770)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski + +## 0.51.2 (2026-05-20) + +### 🩹 Fixes + +- color picker icons (BLO-1189) ([#2762](https://github.com/TypeCellOS/BlockNote/pull/2762)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski + +## 0.51.1 (2026-05-18) + +### 🩹 Fixes + +- backslash newlines when copying from a code block ([#2709](https://github.com/TypeCellOS/BlockNote/pull/2709)) + +### ❤️ Thank You + +- Claude Opus 4.7 (1M context) +- Nick Perez + +## 0.51.0 (2026-05-14) + +### 🚀 Features + +- Trailing block extension rewrite ([#2733](https://github.com/TypeCellOS/BlockNote/pull/2733)) +- **markdown:** replace unified.js with custom markdown parser/serializer ([#2624](https://github.com/TypeCellOS/BlockNote/pull/2624)) +- **react:** configurable portal targets for floating UI ([#2729](https://github.com/TypeCellOS/BlockNote/pull/2729), [#2692](https://github.com/TypeCellOS/BlockNote/issues/2692)) + +### 🩹 Fixes + +- Pasting plain text from VSCode (BLO-366) ([#2713](https://github.com/TypeCellOS/BlockNote/pull/2713)) +- Parse new lines in `text/plain` as line breaks (BLO-1170) ([#2712](https://github.com/TypeCellOS/BlockNote/pull/2712)) +- Code block PDF export (BLO-987) ([#2725](https://github.com/TypeCellOS/BlockNote/pull/2725)) +- Formatting toolbar opening when inserting file block with `trailingBlock: false` (BLO-860) ([#2704](https://github.com/TypeCellOS/BlockNote/pull/2704)) +- numbered list item decorations missed on initial render ([#2734](https://github.com/TypeCellOS/BlockNote/pull/2734)) +- flicker-free mobile formatting toolbar via CSS custom properties ([#2617](https://github.com/TypeCellOS/BlockNote/pull/2617), [#2616](https://github.com/TypeCellOS/BlockNote/issues/2616)) +- add `bn-thread-orphaned` CSS class to distinguish orphaned threads ([#2737](https://github.com/TypeCellOS/BlockNote/pull/2737), [#2735](https://github.com/TypeCellOS/BlockNote/issues/2735)) +- set width attribute on image and video elements in editor render ([#2740](https://github.com/TypeCellOS/BlockNote/pull/2740), [#2726](https://github.com/TypeCellOS/BlockNote/issues/2726)) +- **a11y:** use figure/figcaption for media block captions ([#2717](https://github.com/TypeCellOS/BlockNote/pull/2717)) +- **ai:** loosen serialization of blocks in columns ([#2716](https://github.com/TypeCellOS/BlockNote/pull/2716), [#2718](https://github.com/TypeCellOS/BlockNote/pull/2718)) +- **core:** trigger codeblock input rule on Enter and place cursor inside ([#2686](https://github.com/TypeCellOS/BlockNote/pull/2686)) +- **core:** preserve list item type when pasting into empty list items ([#2722](https://github.com/TypeCellOS/BlockNote/pull/2722), [#2330](https://github.com/TypeCellOS/BlockNote/issues/2330)) +- **core:** unmount editors in transformPasted tests to prevent unhandled error ([e62880b21](https://github.com/TypeCellOS/BlockNote/commit/e62880b21)) +- **drag-n-drop:** support PDF block drag & drop (BLO-893) ([#2714](https://github.com/TypeCellOS/BlockNote/pull/2714)) +- **i18:** improve french translation for empty toggle list ([#2721](https://github.com/TypeCellOS/BlockNote/pull/2721)) +- **markdown:** emit tight lists when serializing blocks to markdown ([#2715](https://github.com/TypeCellOS/BlockNote/pull/2715)) +- **markdown:** skip placeholder text for empty files ([#434](https://github.com/TypeCellOS/BlockNote/pull/434), [#2719](https://github.com/TypeCellOS/BlockNote/pull/2719)) +- **markdown:** stable round-trip for tables, captions, and audio ([#2720](https://github.com/TypeCellOS/BlockNote/pull/2720)) +- **tests:** stabilize webkit keyboard handler tests with programmatic cursor positioning ([#2746](https://github.com/TypeCellOS/BlockNote/pull/2746)) + +### ❤️ Thank You + +- Cyril G +- Manuel Raynaud @lunika +- Matthew Lipski @matthewlipski +- Movm +- Nick Perez +- Nick the Sick @nperez0111 + +## 0.50.0 (2026-05-04) + +### 🚀 Features + +- Dark mode styling for file block wrapper component (BLO-866) ([#2680](https://github.com/TypeCellOS/BlockNote/pull/2680)) +- Drag hendle menu delete button removes all other blocks in selection (BLO-1007) ([#2683](https://github.com/TypeCellOS/BlockNote/pull/2683)) +- Enter moves selection to cell below in tables (BLO-1006) ([#2685](https://github.com/TypeCellOS/BlockNote/pull/2685)) +- additional heading top padding (BLO-1008) ([#2690](https://github.com/TypeCellOS/BlockNote/pull/2690)) +- Code mark input rule edge case (BLO-938) ([#2698](https://github.com/TypeCellOS/BlockNote/pull/2698)) +- **mantine:** upgrade @mantine/core and @mantine/hooks to v9.0.2 ([#2655](https://github.com/TypeCellOS/BlockNote/pull/2655)) + +### 🩹 Fixes + +- Hardcoded strings in comment components (BLO-1033) ([#2681](https://github.com/TypeCellOS/BlockNote/pull/2681)) +- Color naming & CSS (BLO-946) ([#2684](https://github.com/TypeCellOS/BlockNote/pull/2684)) +- link HTML attributes (BLO-915) ([#2687](https://github.com/TypeCellOS/BlockNote/pull/2687)) +- guard hideMenuIfNotFrozen against undefined view state ([#2694](https://github.com/TypeCellOS/BlockNote/pull/2694), [#2699](https://github.com/TypeCellOS/BlockNote/pull/2699)) +- Clicking comment overlapping link opens link (BLO-1091) ([#2696](https://github.com/TypeCellOS/BlockNote/pull/2696)) +- prevent table row drag from moving an extra adjacent row ([#2703](https://github.com/TypeCellOS/BlockNote/pull/2703)) +- **clipboard:** use ProseMirror selection state for Shadow DOM compatibility ([#2677](https://github.com/TypeCellOS/BlockNote/pull/2677)) + +### ❤️ Thank You + +- jt_fox @LimChaeJune +- Matthew Lipski @matthewlipski +- Nick Perez +- Wieland Lindenthal +- Yousef + +## 0.49.0 (2026-04-24) + +### 🚀 Features + +- simplify links by inlining it to BlockNote ([#2623](https://github.com/TypeCellOS/BlockNote/pull/2623)) +- add Unicode quotation mark input rule for quote blocks ([#2673](https://github.com/TypeCellOS/BlockNote/pull/2673)) + +### 🩹 Fixes + +- Inserting link removes comment & add comment button click buggy ([#2620](https://github.com/TypeCellOS/BlockNote/pull/2620), [#2573](https://github.com/TypeCellOS/BlockNote/issues/2573)) +- `useEditorDOMElement` hook ([#2619](https://github.com/TypeCellOS/BlockNote/pull/2619)) +- text color was not applying to table block ([#2663](https://github.com/TypeCellOS/BlockNote/pull/2663)) +- Drag preview blocking drops when overlapping the editor (BLO-996) ([#2670](https://github.com/TypeCellOS/BlockNote/pull/2670)) +- Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116) ([#2628](https://github.com/TypeCellOS/BlockNote/pull/2628), [#2603](https://github.com/TypeCellOS/BlockNote/issues/2603)) +- save file caption/name on every keystroke instead of on close ([#2575](https://github.com/TypeCellOS/BlockNote/pull/2575)) +- prevent FloatingFocusManager from resetting editor selection ([#2525](https://github.com/TypeCellOS/BlockNote/pull/2525), [#2664](https://github.com/TypeCellOS/BlockNote/pull/2664)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- miadnguyen @miadnguyen +- mianguyen +- Nick Perez +- Yousef + +## 0.48.1 (2026-04-16) + +### 🩹 Fixes + +- make CustomChange compatible with prosemirror-changeset 2.4.1 ([#2647](https://github.com/TypeCellOS/BlockNote/pull/2647)) +- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability (CVE-2025-62718) ([c1ef3018a](https://github.com/TypeCellOS/BlockNote/commit/c1ef3018a)) +- **deps:** upgrade nx to 22.6.5 to resolve axios security vulnerability ([#2653](https://github.com/TypeCellOS/BlockNote/pull/2653)) +- **docx-exporter:** omit w:lang when no locale provided instead of defaulting to en-US ([#2651](https://github.com/TypeCellOS/BlockNote/pull/2651)) + +### ❤️ Thank You + +- Claude Opus 4.6 (1M context) +- Nick Perez +- Nick the Sick +- Stephan Meijer @StephanMeijer + +## 0.48.0 (2026-04-13) + +### 🚀 Features + +- upgrade shiki to v4 and prosemirror-highlight to v0.15.1 ([#2625](https://github.com/TypeCellOS/BlockNote/pull/2625)) +- upgrade nx to 22.6.4 and liveblocks to 3.17.0 ([#2627](https://github.com/TypeCellOS/BlockNote/pull/2627)) + +### 🩹 Fixes + +- Image block selection clears on mouse leave in Safari ([#2613](https://github.com/TypeCellOS/BlockNote/pull/2613)) +- Backspace bug when current block is empty and previous block's last child is empty ([#2610](https://github.com/TypeCellOS/BlockNote/pull/2610)) +- allow using latest @tiptap/extension-link version ([1ae8de713](https://github.com/TypeCellOS/BlockNote/commit/1ae8de713)) +- restore depth guard in getParentBlockInfo to prevent RangeError (blo-1103) ([#2585](https://github.com/TypeCellOS/BlockNote/pull/2585)) +- pin better-auth to ~1.4.x to fix docs build ([bda30458a](https://github.com/TypeCellOS/BlockNote/commit/bda30458a)) +- hide side menu on scroll instead of overflow hacks ([#2630](https://github.com/TypeCellOS/BlockNote/pull/2630), [#2043](https://github.com/TypeCellOS/BlockNote/issues/2043)) +- disable default UI when no components context is found ([#2611](https://github.com/TypeCellOS/BlockNote/pull/2611)) +- add .js extension to fast-deep-equal ESM import ([#2641](https://github.com/TypeCellOS/BlockNote/pull/2641)) +- placeholder when overflowing now wraps ([#2291](https://github.com/TypeCellOS/BlockNote/pull/2291)) +- **core:** fix unnesting blocks with siblings (BLO-1017) ([#2601](https://github.com/TypeCellOS/BlockNote/pull/2601)) +- **core:** backspace mid-text next to columnList moves block BLO-1126 ([#2629](https://github.com/TypeCellOS/BlockNote/pull/2629)) + +### 🔥 Performance + +- optimize plugin traversals for large documents BLO-1111 ([#2600](https://github.com/TypeCellOS/BlockNote/pull/2600)) + +### ❤️ Thank You + +- Claude Opus 4.6 +- Claude Opus 4.6 (1M context) +- hedi-ghodhbane @hedi-ghodhbane +- Matthew Lipski @matthewlipski +- Nick Perez +- Nick the Sick @nperez0111 +- Yousef + +## 0.47.3 (2026-03-25) + +### 🩹 Fixes + +- **core:** preserve whitespace edge cases but collapse html formatting newlines (BLO-1065) ([#2551](https://github.com/TypeCellOS/BlockNote/pull/2551), [#2230](https://github.com/TypeCellOS/BlockNote/issues/2230)) + +### ❤️ Thank You + +- Yousef + +## 0.47.2 (2026-03-20) + +### 🩹 Fixes + +- use
/ for toggle block HTML export ([#2524](https://github.com/TypeCellOS/BlockNote/pull/2524)) +- remove @hocuspocus/provider peer dependency by inlining tiptap comment types BLO-1064 ([#2564](https://github.com/TypeCellOS/BlockNote/pull/2564)) +- **core:** slash menu fails in custom blocks after space BLO-1036 ([#2553](https://github.com/TypeCellOS/BlockNote/pull/2553)) +- **i18n:** fix typo in russian translation ([#2560](https://github.com/TypeCellOS/BlockNote/pull/2560)) + +### ❤️ Thank You + +- Claude Opus 4.6 +- Drone +- Yousef + +## 0.47.1 (2026-03-02) + +### 🩹 Fixes + +- typeerror cannot read properties of undefined ([#2522](https://github.com/TypeCellOS/BlockNote/pull/2522)) +- handle more delete key cases ([#2126](https://github.com/TypeCellOS/BlockNote/pull/2126)) +- add delay for `data-active` in collab cursors ([#2383](https://github.com/TypeCellOS/BlockNote/pull/2383)) +- disable slash menu in table content #2408 ([#2504](https://github.com/TypeCellOS/BlockNote/pull/2504), [#2408](https://github.com/TypeCellOS/BlockNote/issues/2408)) +- **ai:** selections broken due to floating-ui focus manager ([#2527](https://github.com/TypeCellOS/BlockNote/pull/2527)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- Nick Perez +- Yousef + +## 0.47.0 (2026-02-23) + +### 🚀 Features + +- update suggestion menu component ([#2397](https://github.com/TypeCellOS/BlockNote/pull/2397)) +- **i18n:** add Persian (fa) localization support ([#2447](https://github.com/TypeCellOS/BlockNote/pull/2447)) +- **i18n:** add Uzbek (uz) localization support ([#2506](https://github.com/TypeCellOS/BlockNote/pull/2506)) + +### 🩹 Fixes + +- prevent nested bullet list icon rendering as emoji on iOS 18+ ([#2394](https://github.com/TypeCellOS/BlockNote/pull/2394), [#2399](https://github.com/TypeCellOS/BlockNote/pull/2399)) +- ignore drag & drop from unrelated events #1968 ([#2346](https://github.com/TypeCellOS/BlockNote/pull/2346), [#1968](https://github.com/TypeCellOS/BlockNote/issues/1968)) +- disable checkbox when editor is not editable #2406 ([#2448](https://github.com/TypeCellOS/BlockNote/pull/2448), [#2406](https://github.com/TypeCellOS/BlockNote/issues/2406)) +- Backspace/enter behaviour in empty block with children ([#2451](https://github.com/TypeCellOS/BlockNote/pull/2451)) +- handle pasting into table cells better, by collapsing their content to inline #2410 ([#2449](https://github.com/TypeCellOS/BlockNote/pull/2449), [#2410](https://github.com/TypeCellOS/BlockNote/issues/2410)) +- **accessibility:** ai combobox aria-activedescendant ([#2413](https://github.com/TypeCellOS/BlockNote/pull/2413)) +- **ai:** no more scrolling to top when opening AI menu ([#2503](https://github.com/TypeCellOS/BlockNote/pull/2503)) +- **docs:** unicode char not rendered in bug template ([f13e270be](https://github.com/TypeCellOS/BlockNote/commit/f13e270be)) + +### ❤️ Thank You + +- Cyril G @Ovgodd +- Dex Devlon @bxff +- Matthew Lipski @matthewlipski +- MDSAM05 @MDSAM05 +- Mohammad RAHMANI @Mrahmani71 +- Nick Perez +- Ogabek @OgabekYuldoshev +- Wouter Vroege +- Yousef + +## 0.46.2 (2026-01-27) + +### 🩹 Fixes + +- deep merge floatingUIOptions using nested spread operators ([#2310](https://github.com/TypeCellOS/BlockNote/pull/2310)) +- Visual differences between live editor and rendered exported HTML ([#2348](https://github.com/TypeCellOS/BlockNote/pull/2348)) +- `BlockNoteViewEditor` mismatched editable value ([#2357](https://github.com/TypeCellOS/BlockNote/pull/2357)) +- add `font-synthesis` for italic & bold in fonts that don't have them specified #2325 ([#2354](https://github.com/TypeCellOS/BlockNote/pull/2354), [#2325](https://github.com/TypeCellOS/BlockNote/issues/2325)) +- disable code block language selector when editor is not editable ([#2351](https://github.com/TypeCellOS/BlockNote/pull/2351)) +- table handles would crash ([#2384](https://github.com/TypeCellOS/BlockNote/pull/2384)) +- update CreateLinkButton to be able to toggle popover visibility ([#2316](https://github.com/TypeCellOS/BlockNote/pull/2316), [#2313](https://github.com/TypeCellOS/BlockNote/issues/2313)) +- add context,nestingLevel to toExternalHTML ([#2373](https://github.com/TypeCellOS/BlockNote/pull/2373)) +- **ai:** re-enable flipping the AIMenu when there is not enough space #2245 ([#2247](https://github.com/TypeCellOS/BlockNote/pull/2247), [#2245](https://github.com/TypeCellOS/BlockNote/issues/2245)) +- **link-toolbar:** prevent Enter from submitting during IME composition ([#2361](https://github.com/TypeCellOS/BlockNote/pull/2361)) + +### ❤️ Thank You + +- hanios123 +- Jean-Baptiste PENRATH +- Matthew Lipski @matthewlipski +- Nick Perez +- Shohei Yoshida @ysds +- Yousef + +## 0.46.1 (2026-01-10) + +This was a version bump only, there were no code changes. + +## 0.46.0 (2026-01-08) + +### 🚀 Features + +- add data-nesting-level to HTML export ([#2329](https://github.com/TypeCellOS/BlockNote/pull/2329)) +- migrate to ai sdk 6 ([#2328](https://github.com/TypeCellOS/BlockNote/pull/2328)) + +### 🩹 Fixes + +- emojipicker can sometimes fail to mount ([575b81cec](https://github.com/TypeCellOS/BlockNote/commit/575b81cec)) +- LinkToolbar Event Listener leak ([#2335](https://github.com/TypeCellOS/BlockNote/pull/2335)) +- when you convert a block into checkListItem via inputRule, it should transfer its content into checkListItem content ([#2331](https://github.com/TypeCellOS/BlockNote/pull/2331)) +- do not return focus back to menu ([484d7da36](https://github.com/TypeCellOS/BlockNote/commit/484d7da36)) +- arrow up on a checklist item should move to the element above BLO-362 ([#2306](https://github.com/TypeCellOS/BlockNote/pull/2306)) +- getPos race condition in React StrictMode ([#2311](https://github.com/TypeCellOS/BlockNote/pull/2311)) +- adjust input rules to be more tolerant to starting whitespace ([#2341](https://github.com/TypeCellOS/BlockNote/pull/2341)) +- **ai:** make sure ShowSelection works ([#2297](https://github.com/TypeCellOS/BlockNote/pull/2297)) +- **xl-email-exporter:** remove redundant sections in email export ([#2323](https://github.com/TypeCellOS/BlockNote/pull/2323)) + +### ❤️ Thank You + +- Nick Perez +- Nick the Sick @nperez0111 +- supernova @tmpluto +- Yousef + +## 0.45.0 (2025-12-17) + +### 🚀 Features + +- **ai:** expand selections to contain words ([#2304](https://github.com/TypeCellOS/BlockNote/pull/2304)) +- **extensions:** extensions can now include other extensions for grouping into one extension ([#2284](https://github.com/TypeCellOS/BlockNote/pull/2284)) + +### 🩹 Fixes + +- an invalidly specified table should not crash the editor ([#2255](https://github.com/TypeCellOS/BlockNote/pull/2255)) +- filter out invalid heading items based on the current block schema in the slash menu #2253 ([#2259](https://github.com/TypeCellOS/BlockNote/pull/2259), [#2253](https://github.com/TypeCellOS/BlockNote/issues/2253)) +- relax shiki package requirements #2279 ([#2280](https://github.com/TypeCellOS/BlockNote/pull/2280), [#2279](https://github.com/TypeCellOS/BlockNote/issues/2279)) +- filter the default tiptap extensions #2282 ([#2283](https://github.com/TypeCellOS/BlockNote/pull/2283), [#2282](https://github.com/TypeCellOS/BlockNote/issues/2282)) +- always include the cursor extension #2244 ([#2260](https://github.com/TypeCellOS/BlockNote/pull/2260), [#2244](https://github.com/TypeCellOS/BlockNote/issues/2244)) +- make `onBeforeChange` return the correct type again ([9009369b1](https://github.com/TypeCellOS/BlockNote/commit/9009369b1)) +- if there is no table block, there is no table handles to show #1055 ([#2281](https://github.com/TypeCellOS/BlockNote/pull/2281), [#1055](https://github.com/TypeCellOS/BlockNote/issues/1055)) +- pass dragHandleMenu prop to DragHandleButton ([#2254](https://github.com/TypeCellOS/BlockNote/pull/2254)) +- html diff error with whitespace ([#2230](https://github.com/TypeCellOS/BlockNote/pull/2230)) +- update regex for checklist items #2288 ([#2305](https://github.com/TypeCellOS/BlockNote/pull/2305), [#2288](https://github.com/TypeCellOS/BlockNote/issues/2288)) +- **email-exporter:** ReadableByteStreamController for safari react-email ([#2295](https://github.com/TypeCellOS/BlockNote/pull/2295)) + +### ❤️ Thank You + +- Max @maqen +- Nick Perez +- Nick the Sick @nperez0111 +- Yousef + +## 0.44.2 (2025-12-09) + +### 🩹 Fixes + +- put back `onBeforeChange` method #2221 ([#2243](https://github.com/TypeCellOS/BlockNote/pull/2243), [#2221](https://github.com/TypeCellOS/BlockNote/issues/2221)) +- Improper accessing of editor DOM element ([#2234](https://github.com/TypeCellOS/BlockNote/pull/2234)) +- make validation errors recoverable by llm ([#2054](https://github.com/TypeCellOS/BlockNote/pull/2054)) +- shadowdom support and example ([#2223](https://github.com/TypeCellOS/BlockNote/pull/2223)) +- ensure numbered list start property always present ([#2241](https://github.com/TypeCellOS/BlockNote/pull/2241), [#2242](https://github.com/TypeCellOS/BlockNote/pull/2242)) +- Suggestion menu positioning ([#2232](https://github.com/TypeCellOS/BlockNote/pull/2232)) +- conditionally access the TableHandles extension from React ([#2248](https://github.com/TypeCellOS/BlockNote/pull/2248)) +- **ai:** upgrade prosemirror-suggest-changes ([#2235](https://github.com/TypeCellOS/BlockNote/pull/2235)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- Nick Perez +- wcyat @sdip15fa +- Yousef + +## 0.44.1 (2025-12-08) + +### 🩹 Fixes + +- clearing selection was not being called when create link button is no longer rendered ([#2217](https://github.com/TypeCellOS/BlockNote/pull/2217)) +- AI menu not updating position on new line ([#2233](https://github.com/TypeCellOS/BlockNote/pull/2233)) +- UI elements not scrolling when editor DOM element is scrollable ([#2231](https://github.com/TypeCellOS/BlockNote/pull/2231)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski + +## 0.44.0 (2025-12-02) + +### 🚀 Features + +- **ai:** Abort requests ([#2213](https://github.com/TypeCellOS/BlockNote/pull/2213)) + +### ❤️ Thank You + +- Yousef + +## 0.43.0 (2025-12-01) + +### 🚀 Features + +- Major Extensions & UI Refactor ([#2143](https://github.com/TypeCellOS/BlockNote/pull/2143)) + +### 🩹 Fixes + +- allow configuring the email body's styles ([#2182](https://github.com/TypeCellOS/BlockNote/pull/2182)) +- **xl-docx-exporter:** improve OOXML interoperability ([#2206](https://github.com/TypeCellOS/BlockNote/pull/2206)) + +### ❤️ Thank You + +- Nick Perez +- Stephan Meijer @StephanMeijer + +## 0.42.3 (2025-11-19) + +### 🩹 Fixes + +- disallow access to the `domElement` or `isFocused` if the editor is unmounted ([#2187](https://github.com/TypeCellOS/BlockNote/pull/2187)) + +### ❤️ Thank You + +- Nick Perez + +## 0.42.2 (2025-11-19) + +### 🩹 Fixes + +- put back mounting system ([#2183](https://github.com/TypeCellOS/BlockNote/pull/2183)) + +### ❤️ Thank You + +- Nick Perez + +## 0.42.1 (2025-11-18) + +### 🩹 Fixes + +- do not error on invalid `backgroundColor` or `textColor` #2176 ([#2179](https://github.com/TypeCellOS/BlockNote/pull/2179), [#2176](https://github.com/TypeCellOS/BlockNote/issues/2176)) +- remove dependency array from comments re-rendering ([#2177](https://github.com/TypeCellOS/BlockNote/pull/2177)) + +### ❤️ Thank You + +- Nick Perez + +## 0.42.0 (2025-11-11) + +### 🚀 Features + +- **yjs:** expose Y.js BlockNote conversion primitives #1866 ([#2166](https://github.com/TypeCellOS/BlockNote/pull/2166), [#1866](https://github.com/TypeCellOS/BlockNote/issues/1866)) + +### 🩹 Fixes + +- Emoji picker issues ([#2092](https://github.com/TypeCellOS/BlockNote/pull/2092)) +- set a default for `blocksToFullHTML` #2100 ([#2101](https://github.com/TypeCellOS/BlockNote/pull/2101), [#2100](https://github.com/TypeCellOS/BlockNote/issues/2100)) +- correctly index blocks that have children fixes #2115 ([#2116](https://github.com/TypeCellOS/BlockNote/pull/2116), [#2115](https://github.com/TypeCellOS/BlockNote/issues/2115)) +- add more lenient parsing for code blocks, to accept newlines #2105 ([#2108](https://github.com/TypeCellOS/BlockNote/pull/2108), [#2105](https://github.com/TypeCellOS/BlockNote/issues/2105)) +- Firefox invisible text cursor after dropping blocks ([#2128](https://github.com/TypeCellOS/BlockNote/pull/2128)) +- parsing `priority` for custom inline content and styles ([#2119](https://github.com/TypeCellOS/BlockNote/pull/2119)) +- `BlockTypeSelect` item filtering based on schema ([#2112](https://github.com/TypeCellOS/BlockNote/pull/2112)) +- deleting last block in column ([#2110](https://github.com/TypeCellOS/BlockNote/pull/2110)) +- **comments:** update the styles for the cursor to be the default cursor ([#2163](https://github.com/TypeCellOS/BlockNote/pull/2163)) +- **comments:** always surface the closest mark to the current position ([#2164](https://github.com/TypeCellOS/BlockNote/pull/2164)) +- **comments:** scrolling bug when clicking comment marks ([#2165](https://github.com/TypeCellOS/BlockNote/pull/2165)) +- **react:** destroy editor instances after two ticks ([#2121](https://github.com/TypeCellOS/BlockNote/pull/2121)) +- **schema-migration:** more robust migration of background-color & text-color attributes ([#2154](https://github.com/TypeCellOS/BlockNote/pull/2154)) +- **unique-id:** do not attempt to append to y-sync plugin transactions ([#2153](https://github.com/TypeCellOS/BlockNote/pull/2153)) + +### ❤️ Thank You + +- Matthew Lipski @matthewlipski +- Nick Perez + +## 0.41.1 (2025-10-09) + +This was a version bump only, there were no code changes. + +## 0.41.0 (2025-10-08) + +### 🚀 Features + +- AI menu auto scrolling ([#2039](https://github.com/TypeCellOS/BlockNote/pull/2039)) +- Shortcut to delete empty table while cells are selected ([#2052](https://github.com/TypeCellOS/BlockNote/pull/2052)) +- **divider:** add a divider block ([#2014](https://github.com/TypeCellOS/BlockNote/pull/2014)) + +### 🩹 Fixes + +- Code block language select value not updating properly ([#2050](https://github.com/TypeCellOS/BlockNote/pull/2050)) +- disable input rules for numbered headings #1789 ([#2032](https://github.com/TypeCellOS/BlockNote/pull/2032), [#1789](https://github.com/TypeCellOS/BlockNote/issues/1789)) +- video parsing and export for markdown ([#1955](https://github.com/TypeCellOS/BlockNote/pull/1955)) +- Reaction picker shown for users who can't react ([#2061](https://github.com/TypeCellOS/BlockNote/pull/2061)) +- Add Mantine dependency to individual examples ([#2070](https://github.com/TypeCellOS/BlockNote/pull/2070)) +- allow listening to `onChange` and other events before the underlying editor is initialized ([#2063](https://github.com/TypeCellOS/BlockNote/pull/2063)) +- toggle and check list item blocks ([#2071](https://github.com/TypeCellOS/BlockNote/pull/2071)) +- added missing fields to implementations in editor schema block specs ([#2046](https://github.com/TypeCellOS/BlockNote/pull/2046)) + +### ❤️ Thank You + +- Héctor Zhuang @Hector-Zhuang +- Matthew Lipski @matthewlipski +- Nick Perez + ## 0.40.0 (2025-09-30) ### 🚀 Features diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fe42686f0..a2e0099d73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,10 +55,10 @@ This diagram illustrates the release workflow for the BlockNote monorepo. Essentially, when the maintainers have decided to release a new version of BlockNote, they will: - 1. Check that the `main` branch is in a releasable state: +1. Check that the `main` branch is in a releasable state: - CI status of main branch is green - Builds are passing - 2. Bump the package versions using the `pnpm run deploy` command. This command will: +2. Bump the package versions using the `pnpm run deploy` command. This command will: 1. Based on semantic versioning, determine the next version number. 2. Apply the new version number to all publishable packages within the monorepo. 3. Generate a changelog for the new version. @@ -80,8 +80,8 @@ The release workflow will: From time to time, you may need to publish a new package to npm. To do this, you cannot just deploy the package to npm, you need to: - 1. Run `nx release version --dry-run` and check that the version number is correct for the package. +1. Run `nx release version --dry-run` and check that the version number is correct for the package. - Once this is done, you can run `nx release version` to actually apply the version bump locally (staged to your local git repo). - 2. Run `nx release changelog --from --dry-run` and check that the changelog is correct for the package. +2. Run `nx release changelog --from --dry-run` and check that the changelog is correct for the package. - Once this is done, you can run the same command without the `--dry-run` flag to actually apply the changelog, commit & push the changes to the `main` branch. - 3. The release workflow will automatically publish the package to npm. +3. The release workflow will automatically publish the package to npm. diff --git a/docs/.env.local.example b/docs/.env.local.example index cb90db5a97..a2dba36b4b 100644 --- a/docs/.env.local.example +++ b/docs/.env.local.example @@ -30,4 +30,6 @@ BETTER_AUTH_URL=http://localhost:3000 # SENTRY_AUTH_TOKEN= NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_API_KEY= -NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL= \ No newline at end of file +NEXT_PUBLIC_BLOCKNOTE_AI_SERVER_BASE_URL= + +TURNSTILE_SECRET_KEY= \ No newline at end of file diff --git a/docs/.eslintrc.json b/docs/.eslintrc.json deleted file mode 100644 index 3f47543412..0000000000 --- a/docs/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "import/extensions": 0 - } -} diff --git a/docs/.gitignore b/docs/.gitignore index 649c9b9df2..c12953bff8 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,45 +1,32 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies +# deps /node_modules -/.pnp -.pnp.js -.yarn/install-state.gz -# testing -/coverage +# generated content +.source -# next.js +# test & build +/coverage /.next/ /out/ - -# production /build +*.tsbuildinfo # misc .DS_Store *.pem - -# debug +/.pnp +.pnp.js npm-debug.log* yarn-debug.log* yarn-error.log* -# local env files +# others .env*.local - -# vercel .vercel - -# typescript -*.tsbuildinfo next-env.d.ts - -# database -*.db - # Sentry Config File .env.sentry-build-plugin + /content/examples/*/* /components/example/generated/ -/.source/ \ No newline at end of file +sqlite.db diff --git a/docs/app/(auth)/layout.tsx b/docs/app/(auth)/layout.tsx new file mode 100644 index 0000000000..715e32a774 --- /dev/null +++ b/docs/app/(auth)/layout.tsx @@ -0,0 +1,10 @@ +import { HomeLayout } from "@/components/fumadocs/layout/home"; +import { baseOptions } from "@/lib/layout.shared"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + +
{children}
+
+ ); +} diff --git a/docs/app/(auth)/signin/page.tsx b/docs/app/(auth)/signin/page.tsx new file mode 100644 index 0000000000..b21d007847 --- /dev/null +++ b/docs/app/(auth)/signin/page.tsx @@ -0,0 +1,18 @@ +import AuthenticationPage from "@/components/AuthenticationPage"; +import { getFullMetadata } from "@/lib/getFullMetadata"; +import { Suspense } from "react"; + +export const metadata = getFullMetadata({ + title: "Sign In", + path: "/signin", +}); + +// Suspense + imported AuthenticationPage because AuthenticationPage is a client component +// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering +export default function Page() { + return ( + + + + ); +} diff --git a/docs/app/(auth)/signin/password/page.tsx b/docs/app/(auth)/signin/password/page.tsx new file mode 100644 index 0000000000..d998c4db48 --- /dev/null +++ b/docs/app/(auth)/signin/password/page.tsx @@ -0,0 +1,17 @@ +import AuthenticationPage from "@/components/AuthenticationPage"; +import { Metadata } from "next"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Password Login", +}; + +// Suspense + imported AuthenticationPage because AuthenticationPage is a client component +// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering +export default function Page() { + return ( + + + + ); +} diff --git a/docs/app/(auth)/signup/page.tsx b/docs/app/(auth)/signup/page.tsx new file mode 100644 index 0000000000..04aceaa07a --- /dev/null +++ b/docs/app/(auth)/signup/page.tsx @@ -0,0 +1,18 @@ +import AuthenticationPage from "@/components/AuthenticationPage"; +import { getFullMetadata } from "@/lib/getFullMetadata"; +import { Suspense } from "react"; + +export const metadata = getFullMetadata({ + title: "Sign Up", + path: "/signup", +}); + +// Suspense + imported AuthenticationPage because AuthenticationPage is a client component +// https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering +export default function Page() { + return ( + + + + ); +} diff --git a/docs/app/(home)/_components/BlockCatalog.tsx b/docs/app/(home)/_components/BlockCatalog.tsx new file mode 100644 index 0000000000..6d1c86e36e --- /dev/null +++ b/docs/app/(home)/_components/BlockCatalog.tsx @@ -0,0 +1,108 @@ +"use client"; +import { + AudioWaveform, + ChevronRight, + Code2, + FileText, + Heading, + Image, + List, + ListOrdered, + ListTodo, + Minus, + Pilcrow, + Puzzle, + Quote, + Table, + Video, +} from "lucide-react"; +import React from "react"; + +const BlockCatalogItem: React.FC<{ name: string; icon: React.ReactNode }> = ({ + name, + icon, +}) => ( +
+
+
+ {icon} +
+ + {name} + +
+); + +export const BlockCatalog: React.FC = () => { + return ( +
+ {/* Subtle decorative elements */} +
+
+
+
+
+ +
+
+
+ 🧩 +
+

+ Build anything, block by block. +

+

+ Every BlockNote document is a collection of blocks—headings, lists, + images, and more. Use the built-in blocks, customize them to fit + your needs, or create entirely new ones. +

+
+ +
+ } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + } + /> + } + /> +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/DigitalCommons.tsx b/docs/app/(home)/_components/DigitalCommons.tsx new file mode 100644 index 0000000000..6bd11d7b06 --- /dev/null +++ b/docs/app/(home)/_components/DigitalCommons.tsx @@ -0,0 +1,127 @@ +"use client"; +import Link from "next/link"; +import React, { useRef, useState } from "react"; + +export const DigitalCommons: React.FC = () => { + const videoRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + + const handlePlayPause = () => { + if (videoRef.current) { + if (isPlaying) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + return ( +
+ {/* Warm gradient overlay to harmonize with cream hero */} +
+ {/* Top edge gradient for smoother transition */} +
+ +
+ {/* Asymmetric layout: content + video (vertically centered) */} +
+ {/* Left: Editorial content */} +
+ {/* Eyebrow with EU flag only */} +
+ 🇪🇺 + + Digital Commons + +
+ + {/* Headline - editorial style */} +

+ Three nations choose
+ + open source + {" "} + to power +
+ their digital future. +

+ + {/* Short punchy copy */} +

+ France, Germany, and the Netherlands partner to build{" "} + + Docs + + , a collaborative writing tool for thousands of public servants.{" "} + BlockNote is the engine. +

+ + {/* Compelling social proof - simpler */} +

+ "Building Digital Commons means better tools, data + sovereignty, and shared progress." +

+ + {/* CTA */} + + Partner with us + + +
+ + {/* Right: Video - vertically centered */} +
+ {/* Glow effect */} +
+ +
+ + + {/* Play button overlay */} + {!isPlaying && ( + + )} +
+
+
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FAQ.tsx b/docs/app/(home)/_components/FAQ.tsx new file mode 100644 index 0000000000..5fbe0bda40 --- /dev/null +++ b/docs/app/(home)/_components/FAQ.tsx @@ -0,0 +1,51 @@ +import React from "react"; + +const faqs = [ + { + question: "Isn't it easier to use a Headless editor framework?", + answer: + "There are a number of really powerful headless text editor frameworks available. In fact, BlockNote is built on Prosemirror and TipTap. However, even when using a headless library, it takes several months and requires deep expertise to build a fully-featured editor with a polished UI that your users expect.", + }, + { + question: "Is BlockNote ready for production use?", + answer: + "BlockNote is used by dozens of companies in production, ranging from startups to large enterprises and public institutions. Also, we didn't reinvent the wheel. The core editor is built on top of Prosemirror - a battle tested framework that powers software from Atlassian, Gitlab, the New York Times, and many others.", + }, + { + question: "Can I add my own extensions to BlockNote?", + answer: + "BlockNote comes with lot of functionality out-of-the-box, but we understand that every use case is different. You can easily customize the built-in UI Components, or create your own custom Blocks, Inline Content, and Styles. If you want to go even further, you can extend the core editor with additional Prosemirror or TipTap plugins.", + }, + { + question: "Is BlockNote really free?", + answer: + "100% of BlockNote is open source. We offer consultancy, support services and commercial licenses for specific XL packages to help sustain BlockNote. Explore our pricing page for more details.", + }, +]; + +export const FAQ: React.FC = () => { + return ( +
+
+
+

+ Questions? +

+
+ +
+ {faqs.map((faq, index) => ( +
+

+ {faq.question} +

+

+ {faq.answer} +

+
+ ))} +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureAI.tsx b/docs/app/(home)/_components/FeatureAI.tsx new file mode 100644 index 0000000000..6f87e80e6b --- /dev/null +++ b/docs/app/(home)/_components/FeatureAI.tsx @@ -0,0 +1,63 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { ContentItem, FeatureWindow } from "./ui/FeatureWindow"; + +export const FeatureAI: React.FC = () => { + const [activeTab, setActiveTab] = useState<"toolbar" | "models" | "human">( + "toolbar", + ); + + const content: Record = { + toolbar: { + type: "video", + src: "/video/ai-select.mp4", + className: "px-4", + }, + models: { + type: "image", + src: "/img/screenshots/home/any_model.png", + alt: "Bring Any Model", + }, + human: { + type: "image", + src: "/img/screenshots/home/human_in_the_loop.png", + alt: "Human in the Loop", + }, + }; + + const tabs = [ + { + id: "toolbar", + icon: , + label: "AI in the Editor", + description: + "Context-aware completions and edits directly in the document.", + }, + { + id: "models", + icon: 🔌, + label: "Bring Any Model", + description: "Connect OpenAI, Anthropic, or your own endpoints.", + }, + { + id: "human", + icon: 🤝, + label: "Human in the Loop", + description: "Users accept, reject, or refine AI suggestions.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={true} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FeatureCollab.tsx b/docs/app/(home)/_components/FeatureCollab.tsx new file mode 100644 index 0000000000..3553d8255f --- /dev/null +++ b/docs/app/(home)/_components/FeatureCollab.tsx @@ -0,0 +1,65 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { ContentItem, FeatureWindow } from "./ui/FeatureWindow"; + +export const FeatureCollab: React.FC<{ + code: { realtime: string }; +}> = ({ code }) => { + const [activeTab, setActiveTab] = useState< + "realtime" | "comments" | "suggestions" + >("realtime"); + + const content: Record = { + realtime: { + type: "code", + file: "CollaborativeEditor.tsx", + code: code.realtime, + }, + comments: { + type: "image", + src: "/img/screenshots/home/comments.png", + alt: "Comments", + }, + suggestions: { + type: "image", + src: "/img/screenshots/home/versioning.png", + alt: "Versioning", + }, + }; + + const tabs = [ + { + id: "realtime", + icon: 👯, + label: "Real-Time Sync", + description: "Yjs-powered with automatic conflict resolution.", + }, + { + id: "comments", + icon: 💬, + label: "Comments", + description: "Inline threads and mentions keep conversations in context.", + }, + { + id: "suggestions", + icon: 📝, + label: "Suggestions & Versioning (coming soon)", + description: + "Track changes, accept or reject edits. Full document history.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={false} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FeatureDX.tsx b/docs/app/(home)/_components/FeatureDX.tsx new file mode 100644 index 0000000000..a98353e0e1 --- /dev/null +++ b/docs/app/(home)/_components/FeatureDX.tsx @@ -0,0 +1,64 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { ContentItem, FeatureWindow } from "./ui/FeatureWindow"; + +export const FeatureDX: React.FC<{ + code: { theming: string; extend: string }; +}> = ({ code }) => { + const [activeTab, setActiveTab] = useState<"types" | "theming" | "extend">( + "types", + ); + + const content: Record = { + types: { + type: "image", + src: "/img/screenshots/home/code-typescript-support.png", + alt: "Type-Safe Schema", + }, + theming: { + type: "code", + file: "Editor.tsx", + code: code.theming, + }, + extend: { + type: "code", + file: "CustomBlock.tsx", + code: code.extend, + }, + }; + + const tabs = [ + { + id: "types", + icon: 📐, + label: "Type-Safe", + description: "Full autocompletion and type inference for custom schemas.", + }, + { + id: "theming", + icon: 🎨, + label: "Bring your Design System", + description: "Works with Mantine, shadcn/ui, or go headless.", + }, + { + id: "extend", + icon: 🔧, + label: "Extend Everything", + description: "Create custom blocks, inline content, menus and more.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={true} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FeatureSection.tsx b/docs/app/(home)/_components/FeatureSection.tsx new file mode 100644 index 0000000000..c7a9973005 --- /dev/null +++ b/docs/app/(home)/_components/FeatureSection.tsx @@ -0,0 +1,101 @@ +import React from "react"; + +interface FeatureTab { + id: string; + icon: React.ReactNode; + label: string; + description: string; +} + +interface FeatureSectionProps { + title: string; + description: string; + tabs: FeatureTab[]; + activeTabId: string; + onTabChange: (id: string) => void; + // The content to display on the right side (Visual or Code) + children: React.ReactNode; + // Optional: Swap order for visual variety (Left/Right) + reverse?: boolean; +} + +export const FeatureSection: React.FC = ({ + title, + description, + tabs, + activeTabId, + onTabChange, + children, + reverse = false, +}) => { + return ( +
+ {/* Left Text & Tabs */} +
+

+ {title} +

+

+ {description} +

+ +
+ {tabs.map((tab) => { + const isActive = activeTabId === tab.id; + // Dynamic styles based on active state could be passed or handled here + // For simplicity, we'll use a generic active style or specific color logic if needed. + // But CodePlayground had specific colors (purple, amber, blue). + // Let's rely on the parent or use a generic active style here for now, + // or we can add a 'color' prop to FeatureTab if we want distinct colors per tab. + + return ( + + ); + })} +
+
+ + {/* Right Visual */} +
+ {/*
*/} +
+ {children} +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/FeatureUX.tsx b/docs/app/(home)/_components/FeatureUX.tsx new file mode 100644 index 0000000000..3bfe2d00ff --- /dev/null +++ b/docs/app/(home)/_components/FeatureUX.tsx @@ -0,0 +1,60 @@ +"use client"; +import React, { useState } from "react"; +import { FeatureSection } from "./FeatureSection"; +import { ContentItem, FeatureWindow } from "./ui/FeatureWindow"; +export const FeatureUX: React.FC = () => { + const [activeTab, setActiveTab] = useState<"components" | "ai" | "blocks">( + "components", + ); + + const content: Record = { + components: { + type: "video", + src: "/video/batteries-included.mp4", + }, + ai: { + type: "video", + src: "/video/ai-select.mp4", + className: "px-4", + }, + blocks: { + type: "video", + src: "/video/dragdrop.mp4", + }, + }; + + const tabs = [ + { + id: "components", + icon: 🔋, + label: "Ready to Use", + description: + "Slash menus, formatting toolbars, and drag handles work instantly.", + }, + { + id: "ai", + icon: , + label: "AI Assistance", + description: "Write and redact content with AI.", + }, + { + id: "blocks", + icon: 🧱, + label: "Block-Based", + description: "Drag, drop, and nest content blocks.", + }, + ]; + + return ( + setActiveTab(id as any)} + reverse={false} + > + + + ); +}; diff --git a/docs/app/(home)/_components/FrameworkPill.tsx b/docs/app/(home)/_components/FrameworkPill.tsx new file mode 100644 index 0000000000..82e6b90216 --- /dev/null +++ b/docs/app/(home)/_components/FrameworkPill.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const FrameworkPill: React.FC<{ + name: string; + color: string; + icon?: React.ReactNode; +}> = ({ name, color, icon }) => ( +
+ {icon ||
} + {name} +
+); diff --git a/docs/app/(home)/_components/Hero.tsx b/docs/app/(home)/_components/Hero.tsx new file mode 100644 index 0000000000..2bb0dc5b52 --- /dev/null +++ b/docs/app/(home)/_components/Hero.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import React from "react"; +import { HeroVideo } from "./HeroVideo"; +import { TextLoop } from "./TextLoop"; + +export const Hero: React.FC = () => { + const BADGES = [ + { icon: "⭐️", text: "100k+ weekly installs" }, + { icon: "🛡️", text: "100% Open source & self-hostable" }, + { icon: "✨", text: "AI Ready" }, + ]; + + return ( +
+ {/* Hero Section */} +
+ {/* Passive Neural Background */} +
+ {/* Badge */} + + {BADGES.map((badge, index) => ( +
+ + {badge.icon} + {badge.text} + +
+ ))} +
+ +

+ Build a Notion-style{" "} + editor in minutes. +

+

+ The AI-native, open source rich + text editor for React. Add a{" "} + fully customizable modern block-based editing + experience to your product that users will love. +

+ +
+ + View Demo + + → + + + + Documentation + +
+
+
+ +
+
+
+ ); +}; diff --git a/docs/app/(home)/_components/HeroVideo.tsx b/docs/app/(home)/_components/HeroVideo.tsx new file mode 100644 index 0000000000..a36cccbe73 --- /dev/null +++ b/docs/app/(home)/_components/HeroVideo.tsx @@ -0,0 +1,77 @@ +"use client"; +import Link from "next/link"; +import React from "react"; + +export const HeroVideo: React.FC = () => { + return ( + <> + +
+ {/* Editor Placeholder */} + {/* Editor Preview */} + +
+
+ {/* Browser Chrome */} +