You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Block WYSIWYG editor powered by TipTap. Blocks have PHP classes that define rendering, validation and their JS; developers and packages add blocks; multi-column layouts; Thaana JTK keyboard support; Blade and Inertia React.
Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.
Studied: ../stencil (closest shape: PHP + React, committed dist, manifest), ../manifest contracts (#13 there), forms v2 components, @javaabu/forms field hooks, and Onu's editor (@javaabu/onu/editor).
Architecture
Content format. TipTap/ProseMirror JSON ({"type":"doc","content":[…]}), the same shape Onu's editors use. Each node type is a block name; marks are bold, italic, underline, strike, code, link. Stored as JSON on the model.
PHP (Javaabu\BlockEditor\)
Blocks\Block: one class per node type. It declares the name, group, a content expression (inline*, block+, listItem+, column+, or empty for leaf blocks), its attributes (Attribute::string('url')->url()->required(): type, default, rules, settings-panel input), the Blade view, the React component name, the JS module (a manifest script asset) that implements it in the editor, and its title, icon and keywords for the inserter.
BlockEditor manager and facade: register(), replace(), forget(), get(), blocks(), plus named profiles (allow-lists such as default and minimal) that fields, casts and rules accept.
Sanitizer: drops unknown nodes, marks and attrs, casts attrs to their types, removes unsafe URLs, allow-lists embed providers, and runs the HTML block through symfony/html-sanitizer.
Rules\BlockContent: validates structure against content expressions and attrs against each block's rules. Errors are reported on the field (Block 3 (Button): …) and on the JSON path (body.content.2.attrs.url), so the editor can mark the bad block.
Casts\AsBlockDocument: casts to Document and sanitizes on set.
Rendering: <x-block-editor::content :content="$post->body" /> renders one overridable Blade view per block (block-editor::blocks.{name}).
Form field: <x-block-editor::editor name="body" :model="$post" /> builds on forms v2's Input (model binding, old input, errors, form group). Stencil field: Javaabu\BlockEditor\Stencil\BlockEditor::make('body')->profile('default').
JS (TypeScript, resources/js, imported as @javaabu/block-editor through manifest; prebuilt dist committed for apps without Vite)
defineBlock({ name, extension, menu, render }) / registerBlock(): a global registry, so blocks shipped by other packages' scripts (mediapicker) and the editor share one registry, even across prebuilt bundles.
createBlockEditor(element, { config, value, onChange, … }): a framework-agnostic editor on @tiptap/core. It covers the toolbar, the / slash menu, a "+" inserter and a block handle (drag, move, duplicate, delete, and Mod-Shift-↑/↓), plus a block settings panel generated from the PHP attribute definitions, so PHP-only blocks work without JS. Nested blocks (columns) work with the handle too.
Blade: resources/js/blade/editor.ts auto-mounts [data-block-editor] and keeps a hidden input in sync.
React: <BlockEditor> (a thin wrapper over the same core, so both stacks behave identically), <BlockEditorField> for @javaabu/forms (binding, errors, usePublishField), a stencil component for the block-editor type, and <BlockContent>, a React renderer that outputs the same markup as the Blade views. Shared fixtures test PHP and React output against the same HTML.
Reused from Onu (@javaabu/onu/editor): ThaanaKeyboard (JTK phonetic), AutoDirection (per-block dir, lists, quotes and tables resolved from content), toEmbedUrl, isSafeImageUrl, isSafeUrl and the theme tokens. Onu's BlockEditor component itself isn't reused: its block menu is a closed set and can't take registered blocks, and that's the core of this package. I'll suggest an extension point upstream later rather than block on it.
The PHP config (blocks, attributes, translated UI strings) is passed to the editor as JSON: a data- attribute in Blade, a prop in React. en and dv strings live in lang/, with TS copies generated and checked in CI.
Core blocks (#5): paragraph, heading, bullet/ordered list, quote, code, table, divider, image (URL), embed (YouTube/Vimeo, extensible), button, callout and HTML (sanitized). Layout (#6):columns / column, with presets (1:1, 2:1, 1:2, 1:1:1, …), stacking at a breakpoint, logical CSS and RTL-aware.
#1 foundations → #3 PHP blocks and registry (the registration contract is posted here as soon as it's pushed) → #4 storage, validation, sanitizing, rendering → #2 editor core → #5 core blocks → #6 columns → #7 Thaana → #8 form fields → #12 → #11 → #9, #10.
Decisions and open questions
Depends on javaabu/stencil (require, not optional), because the stencil field is part of the package.
No controllers, routes or pages, so there are no policies or permissions. The "views" are the editor field and the content renderer, and both ship in Blade and React. A views stack config option means nothing here, because the stack is whichever component you render. Recorded on Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #12.
The core image block takes a URL. javaabu/mediapicker can replace('image', …) or add its own blocks.
For javaabu/mediapicker, javaabu/post-types and app blocks. Require it with "repositories": [{"type": "vcs", "url": "https://github.com/Javaabu/block-editor"}] and "javaabu/block-editor": "^1.0@dev".
Content is TipTap/ProseMirror JSON ({"type":"doc","content":[…]}). A block is a node type with a PHP class (attributes, validation, Blade rendering, menu metadata) and optionally a JS module (TipTap extension and React renderer).
PHP
useJavaabu\BlockEditor\Blocks\Attribute;
useJavaabu\BlockEditor\Blocks\Block;
class Gallery extends Block
{
protectedstring$name = 'gallery'; // node type in the JSON (camelCase)protectedstring$content = ''; // ProseMirror content expression; '' = leaf (atom)// 'inline*' text, 'block+' nested blocks, 'galleryItem+'protectedstring$category = 'media'; // text | media | embed | layout | advancedprotectedstring$icon = 'images'; // Lucide nameprotectedarray$keywords = ['photos'];
protected ?string$view = 'mediapicker::blocks.gallery'; // default block-editor::blocks.{kebab-name}protected ?string$script = 'mediapicker::gallery-block'; // manifest script asset with the JS halfpublicfunctionattributes(): array
{
return [
'media' => Attribute::json(), // hidden, kept as is'columns' => Attribute::integer()->default(3)->min(1)->max(6),
'caption' => Attribute::string()->max(500)->thaana(), // shown in the settings panel'link' => Attribute::url(), // unsafe schemes are rejected and dropped'style' => Attribute::enum(['grid', 'masonry'])->default('grid')->required(),
];
}
publicfunctionrules(): array { return ['media' => ['required', 'array', 'min:1']]; } // extra rulespublicfunctionsanitize(Node$node, Sanitizer$sanitizer): ?Node { return$node; } // last pass; null drops it
}
// In a service provider (register() or boot())BlockEditor::register(Gallery::class); // or add it to config('block-editor.blocks')BlockEditor::register(newMediaImage); // same name as an existing block replaces it (e.g. 'image')BlockEditor::forget('html');
BlockEditor::profile('comment', ['paragraph', 'bulletList', 'orderedList'], ['bold', 'italic', 'link']);
Child node types (e.g. galleryItem) are blocks too, with protected string $group = '' and protected bool $insertable = false. They're allowed wherever a parent names them.
The Blade view gets $node, $attrs, $content (rendered children), $block and $renderer ($renderer->attributes([...])).
Content is sanitized on save (AsBlockDocument cast) and again on render. It's validated by new BlockContent($profile). Errors come back on the field and on the JSON path (body.content.2.attrs.media).
<x-block-editor::editor> requires each profile block's $script, so register that asset with manifest in your provider.
JS (the block's $script, importing from @javaabu/block-editor)
import{Node}from"@tiptap/core"import{atomView,attributesFrom,dataAttrs,defineBlock,registerBlock,settingsPrompt}from"@javaabu/block-editor"registerBlock(defineBlock({name: "gallery",settingsOnInsert: true,// open the settings panel after insertingextensions: ({ block })=>[Node.create({name: "gallery",group: "block",atom: true,draggable: true,addAttributes: ()=>attributesFrom(block),// attrs + defaults from the PHP configparseHTML: ()=>[{tag: 'div[data-block="gallery"]'}],renderHTML: ({ node })=>["div",dataAttrs("gallery",node.attrs)],addNodeView: ()=>(props)=>atomView(props,(node)=>/* preview DOM */settingsPrompt(props,"Pick images")),}),],insert: (editor,{ item, openSettings })=>{/* optional; default inserts the node with defaults */},render: GalleryComponent,// React renderer for <BlockContent>; same markup as the Blade view}),)
The registry is global (globalThis), so blocks from other packages' scripts and separately built bundles reach every editor on the page.
A block with a PHP class and no JS still works: the editor builds a generic node from its config (a card plus the settings panel for leaves; a wrapper for content).
Embed providers: Embed::provider('loom', fn (string $url) => …) in PHP, and registerEmbedProvider('loom', (url) => …) in JS.
React: import { BlockContent, BlockEditor, BlockEditorField, StencilBlockEditor } from "@javaabu/block-editor/react".
Final summary: javaabu/block-editor v1.0 is done on main
Every next-gen issue is closed except #13 (docs.javaabu.com registration, left for you). CI is green on the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, a guard against javaabu/helpers and javaabu/settings v1 in production dependencies, and a JS job (lint, format, typecheck, Vitest with coverage, examples build, committed dist check). 79 PHPUnit tests and 64 Vitest tests. The editor was also checked in headless Chromium: slash menu, inserter, settings panel, Thaana typing, keyboard moves with announcements, and drag and drop.
What was built
Blocks in PHP (Block classes in PHP #3): Block (name, group, content expression, marks, Attributes with types, defaults, rules, settings inputs and Thaana, Blade view, $script, menu metadata) and Mark. A BlockEditor registry with profiles and editorConfig(). The registration contract is in this comment.
Storage, validation, sanitizing, rendering (Storage, casting and rendering #4): TipTap JSON, the AsBlockDocument cast, Document/Node, the BlockContent rule (errors on the field and on the JSON path), and a sanitizer (symfony/html-sanitizer for HTML blocks) that runs on save and on render. Blade views and the React BlockContent produce identical markup, checked against shared fixtures.
Editor (TipTap-based editor core #2): a framework-free TipTap editor (createBlockEditor) with toolbar, / menu, + inserter, drag handle, block actions, settings panel generated from attributes, links, shortcuts, live-region announcements, and server errors on blocks. A Blade entry and a React wrapper. Onu's ThaanaKeyboard, AutoDirection, CalloutNode and Embed are reused.
npm scopes (manifest:check asks Blade apps to install the React/Inertia dependencies of package pages manifest#17): @javaabu/forms is now declared with npm(..., for: 'pages'), so manifest:check asks for it only in apps with an Inertia adapter. It was previously missing from the declarations altogether. TipTap, @javaabu/onu, react and react-dom stay in the assets scope: the Blade entry imports @javaabu/onu/editor, a single module that also imports React, @tiptap/react and @tiptap/static-renderer, so Blade apps building from source need them until Editor: a React-free entry for the TipTap extensions, and custom block types in BlockEditor onu#62 lands. tests/Feature/NpmDependenciesTest.php checks the declared scopes against the imports in resources/js and runs manifest:check for a Blade app and an Inertia app.
CSS on Inertia pages (Inertia: theme CSS isn't loaded; starter menu label stays English in Dhivehi n4ff4h/cms#12): BlockEditor and BlockContent import the stylesheet, so Vite bundles it with any page that renders them. This includes the fields, through BlockEditor. Blade is unchanged. cms can drop its @import "../../vendor/javaabu/block-editor/resources/css/block-editor.css" line.
Activity
n4ff4h commentedon Nov 5, 2026
Implementation plan
Studied:
../stencil(closest shape: PHP + React, committeddist, manifest),../manifestcontracts (#13 there), forms v2 components,@javaabu/formsfield hooks, and Onu's editor (@javaabu/onu/editor).Architecture
Content format. TipTap/ProseMirror JSON (
{"type":"doc","content":[…]}), the same shape Onu's editors use. Each nodetypeis a block name; marks arebold,italic,underline,strike,code,link. Stored as JSON on the model.PHP (
Javaabu\BlockEditor\)Blocks\Block: one class per node type. It declares the name, group, a content expression (inline*,block+,listItem+,column+, or empty for leaf blocks), its attributes (Attribute::string('url')->url()->required(): type, default, rules, settings-panel input), the Blade view, the React component name, the JS module (a manifest script asset) that implements it in the editor, and its title, icon and keywords for the inserter.BlockEditormanager and facade:register(),replace(),forget(),get(),blocks(), plus named profiles (allow-lists such asdefaultandminimal) that fields, casts and rules accept.Document/Nodevalue objects:Document::make($json),->blocks(),->toHtml(),->toText(),->isEmpty(),->toArray().Sanitizer: drops unknown nodes, marks and attrs, casts attrs to their types, removes unsafe URLs, allow-lists embed providers, and runs the HTML block throughsymfony/html-sanitizer.Rules\BlockContent: validates structure against content expressions and attrs against each block's rules. Errors are reported on the field (Block 3 (Button): …) and on the JSON path (body.content.2.attrs.url), so the editor can mark the bad block.Casts\AsBlockDocument: casts toDocumentand sanitizes on set.<x-block-editor::content :content="$post->body" />renders one overridable Blade view per block (block-editor::blocks.{name}).<x-block-editor::editor name="body" :model="$post" />builds on forms v2'sInput(model binding, old input, errors, form group). Stencil field:Javaabu\BlockEditor\Stencil\BlockEditor::make('body')->profile('default').JS (TypeScript,
resources/js, imported as@javaabu/block-editorthrough manifest; prebuiltdistcommitted for apps without Vite)defineBlock({ name, extension, menu, render })/registerBlock(): a global registry, so blocks shipped by other packages' scripts (mediapicker) and the editor share one registry, even across prebuilt bundles.createBlockEditor(element, { config, value, onChange, … }): a framework-agnostic editor on@tiptap/core. It covers the toolbar, the/slash menu, a "+" inserter and a block handle (drag, move, duplicate, delete, and Mod-Shift-↑/↓), plus a block settings panel generated from the PHP attribute definitions, so PHP-only blocks work without JS. Nested blocks (columns) work with the handle too.resources/js/blade/editor.tsauto-mounts[data-block-editor]and keeps a hidden input in sync.<BlockEditor>(a thin wrapper over the same core, so both stacks behave identically),<BlockEditorField>for@javaabu/forms(binding, errors,usePublishField), a stencil component for theblock-editortype, and<BlockContent>, a React renderer that outputs the same markup as the Blade views. Shared fixtures test PHP and React output against the same HTML.@javaabu/onu/editor):ThaanaKeyboard(JTK phonetic),AutoDirection(per-blockdir, lists, quotes and tables resolved from content),toEmbedUrl,isSafeImageUrl,isSafeUrland the theme tokens. Onu'sBlockEditorcomponent itself isn't reused: its block menu is a closed set and can't take registered blocks, and that's the core of this package. I'll suggest an extension point upstream later rather than block on it.data-attribute in Blade, a prop in React.enanddvstrings live inlang/, with TS copies generated and checked in CI.Core blocks (#5): paragraph, heading, bullet/ordered list, quote, code, table, divider, image (URL), embed (YouTube/Vimeo, extensible), button, callout and HTML (sanitized). Layout (#6):
columns/column, with presets (1:1, 2:1, 1:2, 1:1:1, …), stacking at a breakpoint, logical CSS and RTL-aware.Public API sketch
Order of work
#1 foundations → #3 PHP blocks and registry (the registration contract is posted here as soon as it's pushed) → #4 storage, validation, sanitizing, rendering → #2 editor core → #5 core blocks → #6 columns → #7 Thaana → #8 form fields → #12 → #11 → #9, #10.
Decisions and open questions
require, not optional), because the stencil field is part of the package.viewsstack config option means nothing here, because the stack is whichever component you render. Recorded on Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #12.replace('image', …)or add its own blocks.n4ff4h commentedon Nov 5, 2026
Contract: block registration is on
mainFor javaabu/mediapicker, javaabu/post-types and app blocks. Require it with
"repositories": [{"type": "vcs", "url": "https://github.com/Javaabu/block-editor"}]and"javaabu/block-editor": "^1.0@dev".Content is TipTap/ProseMirror JSON (
{"type":"doc","content":[…]}). A block is a node type with a PHP class (attributes, validation, Blade rendering, menu metadata) and optionally a JS module (TipTap extension and React renderer).PHP
string,text,url,integer,number,boolean,enum,json. Builders:default(),required(),hidden(),thaana(),label(),help(),input()(text, textarea, url, number, checkbox, select, code),min(),max(),options(),rules().galleryItem) are blocks too, withprotected string $group = ''andprotected bool $insertable = false. They're allowed wherever a parent names them.$node,$attrs,$content(rendered children),$blockand$renderer($renderer->attributes([...])).AsBlockDocumentcast) and again on render. It's validated bynew BlockContent($profile). Errors come back on the field and on the JSON path (body.content.2.attrs.media).<x-block-editor::editor>requires each profile block's$script, so register that asset with manifest in your provider.JS (the block's
$script, importing from@javaabu/block-editor)globalThis), so blocks from other packages' scripts and separately built bundles reach every editor on the page.Embed::provider('loom', fn (string $url) => …)in PHP, andregisterEmbedProvider('loom', (url) => …)in JS.import { BlockContent, BlockEditor, BlockEditorField, StencilBlockEditor } from "@javaabu/block-editor/react".Using it for content (post-types)
n4ff4h commentedon Nov 5, 2026
Final summary: javaabu/block-editor v1.0 is done on
mainEvery
next-genissue is closed except #13 (docs.javaabu.com registration, left for you). CI is green on the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, Pint, a guard againstjavaabu/helpersandjavaabu/settingsv1 in production dependencies, and a JS job (lint, format, typecheck, Vitest with coverage, examples build, committeddistcheck). 79 PHPUnit tests and 64 Vitest tests. The editor was also checked in headless Chromium: slash menu, inserter, settings panel, Thaana typing, keyboard moves with announcements, and drag and drop.What was built
Block(name, group, content expression, marks,Attributes with types, defaults, rules, settings inputs and Thaana, Blade view,$script, menu metadata) andMark. ABlockEditorregistry with profiles andeditorConfig(). The registration contract is in this comment.AsBlockDocumentcast,Document/Node, theBlockContentrule (errors on the field and on the JSON path), and a sanitizer (symfony/html-sanitizer for HTML blocks) that runs on save and on render. Blade views and the ReactBlockContentproduce identical markup, checked against shared fixtures.createBlockEditor) with toolbar,/menu,+inserter, drag handle, block actions, settings panel generated from attributes, links, shortcuts, live-region announcements, and server errors on blocks. A Blade entry and a React wrapper. Onu'sThaanaKeyboard,AutoDirection,CalloutNodeandEmbedare reused.dv, direction resolved the same way by PHP, React and the editor, logical CSS only, and English and Dhivehi strings.<x-block-editor::editor>(forms v2),<x-block-editor::content>,BlockEditorField(@javaabu/forms),BlockEditor,BlockContent, and the stencilBlockEditorFieldwith theStencilBlockEditorReact type. Assets ship through manifest, with a prebuiltdist.llms.txt, a Boost guideline and thejavaabu-block-editorskill, AGENTS.md, and runnableexamples/.Decisions and out of scope
BlockEditorcomponent isn't reused, because its menus are a closed set. The editor UI is framework-free DOM styled with Onu's tokens, so Blade pages don't need React at runtime. Suggested upstream: Editor: a React-free entry for the TipTap extensions, and custom block types in BlockEditor onu#62.imageto replace it).viewsstack config option and no controllers or policies: the package has no pages, and you choose the stack by the component you use (see Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #12).useForm(), bind it withvalue/onChangeuntil Export useFieldState, splitFieldProps and usePublishField for custom fields react-forms#23 lands (non-blocking).What you need to do
CODECOV_TOKENavailable to this repo (org secret) so uploads and the badge work.docs/advanced-usage/translations.md#needs-review.n4ff4h commentedon Nov 6, 2026
Follow-up in e7dfe81:
@javaabu/formsis now declared withnpm(..., for: 'pages'), somanifest:checkasks for it only in apps with an Inertia adapter. It was previously missing from the declarations altogether. TipTap,@javaabu/onu,reactandreact-domstay in the assets scope: the Blade entry imports@javaabu/onu/editor, a single module that also imports React,@tiptap/reactand@tiptap/static-renderer, so Blade apps building from source need them until Editor: a React-free entry for the TipTap extensions, and custom block types in BlockEditor onu#62 lands.tests/Feature/NpmDependenciesTest.phpchecks the declared scopes against the imports inresources/jsand runsmanifest:checkfor a Blade app and an Inertia app.BlockEditorandBlockContentimport the stylesheet, so Vite bundles it with any page that renders them. This includes the fields, throughBlockEditor. Blade is unchanged. cms can drop its@import "../../vendor/javaabu/block-editor/resources/css/block-editor.css"line.