Repository navigation
Conversation
Splits the "tunes as plugins" work from the review on #157 into three OpenSpec changes: - plugin-block-data: BlockNode's `tunes` store becomes `plugins` (per-block data keyed by plugin name), created on first write, readable and writable through BlocksAPI, undoable, and synchronized through OT. Renames the model's tune vocabulary (PluginDataNode / PluginDataIndex / PluginDataModifiedEvent) and carries v2 `tunes` data over in composeDataFromVersion2. - block-settings-ui: BlockSettingsUI exposes the block settings menu as a plugin API, ToolbarUI grows a settings button, and the built-in move/delete actions become @editorjs/block-actions-plugin. Assumes the e2e/aria branch has landed. - remove-block-tunes: deletes ToolType.Tune, BlockTune, BlockTuneFacade and the tool `tunes` option. Independent of the other two. Artifacts only; no implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design and the capability spec already required a null-prototype store, an empty-name check and attaching the node listener before the first update. The task list did not, so the implementation steps could miss them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
||
| ## What Changes | ||
|
|
||
| - **Model — per-block plugin data.** Rename the BlockNode `tunes` store to `plugins`, keyed by plugin `name`. Writing to an absent entry creates it. Setting a key to `undefined` removes it, and empty entries are left out of serialization. **BREAKING**: the serialized block key `tunes` → `plugins`. |
There was a problem hiding this comment.
I'm a bit uneasy about using the word plugins in data to store "tunes" data. "Plugin" is a code-organization term, and from a data-model perspective it feels a bit out of place. Maybe meta would be better? Or something closer to the data model. In the code, these could all still be plugins. Though I realize that might cause some confusion.
There was a problem hiding this comment.
Do you suggest to rename per-tool configuration as well? We had tunes property in the tool config to list tunes for blocks of the type. Now it's going to be plugins
Something like
EditorJS.use(Paragraph, { plugins: ['spoiler'] }
I think meta wouldn't work there. Or it would be plugins in the config, but meta in the model and the output?
I also would reserve meta for actual metadata (e.g. createdAt, author, etc). I agree from model perspective plugins is not the right term, but from the developer perspective I would load plugins and would expect plugins in the output. So maybe naming consistency is more important here than contextual correctness
There was a problem hiding this comment.
ok, let's try "plugins" in data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The e2e/ARIA branch landed as #187, which satisfies this change's first precondition and invalidates the second. Its ARIA requirements turned out to live beside `Floating toolbar` rather than inside it, so the delta was not about to revert them -- but it did duplicate one of their scenarios and put the settings button's ARIA assertions in the wrong requirement. The delta now modifies all three merged requirements, each header matching verbatim so the archive fold still applies: behaviour stays in `Floating toolbar`, accessible names in `Accessible floating toolbar`, and roving tabindex plus `aria-expanded` in `The block actions toolbar is a single tab stop`. Drops the duplicated tab-stop scenario and adds the block-settings counterpart of the toolbox's "keys from inside the popover are left alone". Also resolves the "block actions" collision #187 made concrete. `main` now ships that phrase as the toolbar container's accessible name, asserted in two e2e suites and carried in a requirement title; the move/delete package was only spec text. It becomes `@editorjs/default-block-settings-plugin` (`default-block-settings`), which is what it actually is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three implementation branches had accumulated spec changes of their own: delta rewrites made once the code showed what the requirement should say, and the hand-edits to live specs that the mechanical archive fold cannot do -- preambles naming the tune tool kind, the bundle's default plugin list and the note claiming `ui` has no test suite. Specs belong in this PR, so they are here and the implementation branches keep only their task lists. Nothing is new: every hunk already existed further up the stack and moves down unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s stack Review of the three implementation PRs turned up behaviour that the specs did not cover, so it is written down before it is built: - block selection is dispatched on focus as well as hover, because the settings button acts on "the selected block" and a keyboard-only user never produces one - the settings button closes the menu it opened, per the menu button pattern - `BlocksAPI` gains `getToolByIndex`, so reading one block's tool name stops going through `DocumentAPI.data`, which serializes the whole document - a plugin data write that changes nothing emits no event, and so records neither an operation nor an undo step - a key removal is specified to survive the wire, where `undefined` does not The UI package's `MenuConfig` is renamed `BlockSettingsMenuConfig` in the spec text: `@editorjs/sdk` already exports a different type by that name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Plugin data operations | ||
| `CollaborationManager` SHALL translate a `PluginDataModifiedEvent` into a `Modify` `Operation` on its `PluginDataIndex`, with `payload` set to the new value and `prevPayload` to the previous value. `OperationsTransformer` SHALL shift a plugin data operation's block index against block insertions and removals the same way it does for data index operations, SHALL turn it into a `Neutral` operation when its block is removed, and SHALL leave two concurrent plugin data operations untransformed against each other. Transformation SHALL also accept a plugin data operation as the operation being transformed *against*, rather than rejecting its index kind. The `Modify` operation payload type SHALL admit any JSON value, including primitives. |
There was a problem hiding this comment.
2 concurrent operations may lead to clients divergence:
- clients
AandBwrite concurrently - server applies
A, thenB - client
Balready applied own op locally, then receives originalA(applied beforeB) and ends withA
Regular block data already handles this case: transformAgainstDataOperation turns an incoming operation into Neutral when it targets the same key as an unsent local one.
There was a problem hiding this comment.
You're right, and the spec sentence was mandating it. I traced it through to be sure:
- B writes locally, its operation unsent (
revundefined). OTClient#onMessagetransforms every incoming remote operation against the pending local ones — but the plugin data branch returned them untouched, so A's value was applied on B.- B's own operation is never echoed back to it (
message.payload.userId === this.#userIdreturns early), so nothing restoredfalse. - Server
false, Afalse, Btrue. Exactly the divergence you described.
Fixed by giving the plugin data branch the rule #transformAgainstDataOperation already uses: an against-operation with no revision counts as Infinity, so the incoming one becomes Neutral and the unsent local write stands. Scoped to the same block, plugin and key, since nothing else describes the same value — a write to anchors.visible must not neutralize one to anchors.id.
The delta now states that rule instead of the old "left untransformed", with scenarios for the same-key and different-key cases. Implementation and tests are in #192. The test that proves it: should become Neutral against an unsent write to the same key fails against the previous return Operation.from(operation), and three more fail if the index comparison is dropped, so the narrowing is covered too.
There was a problem hiding this comment.
Spec and code match now, but plugin-block-data/design.md (line 58 and the same-key Risks entries) still says plugin data ops stay untransformed.
| - *Alternative:* plugins push static item lists at registration. Rejected, because items depend on the block and its state. | ||
|
|
||
| Wiring follows the Toolbox pattern: | ||
| 1. `ToolbarUI`'s settings button dispatches `ui:block-settings:open` with the hovered block index. |
There was a problem hiding this comment.
This contradicts specs/block-settings/spec.md and specs/ui/spec.md.
Spec claims that
BlockSettingsUI- renders button
- tracks block selection
- announces button via event
ToolbarUI- reassigns its tab stop when the button joins
Design claims that ToolbarUI owns button and hovered block index.
I suppose design is the right one, but specs require sync.
There was a problem hiding this comment.
Right that they contradict — but it's the other way round: the specs are current and design.md is stale, from before the ARIA work was folded into this change.
The code agrees with the specs: the button, its aria-haspopup/aria-expanded, the focus-before-open and the block it acts on all live in BlockSettingsUI, and ToolbarUI only mounts the two elements it's handed and reassigns its tab stop. There's no ui:block-settings:open dispatch from the toolbar at all.
I've updated design.md to match, and added the reasoning that was missing: a menu button's aria-expanded has to track the menu, and the menu's open state is the plugin's — split across the two, every open and close needs a round trip through the event bus to keep an attribute honest, and the button can end up reporting a block the menu wasn't built for. ToolbarUI treats it as a control of the container like its own plus button: named and operated by its owner, positioned and tab-managed by the toolbar.
Its remaining MenuConfig references are now BlockSettingsMenuConfig, and the claim that @editorjs/sdk owns the item shape is corrected — the SDK's MenuConfig is a different, narrower type for inline tools, which is why the UI's was renamed apart from it.
There was a problem hiding this comment.
proposal.md (lines 18–19, 31, 37) and tasks.md (3.1, line 49) still put the button in ToolbarUI.
Four threads from the review on #189. Concurrent plugin data writes: the delta required the transformer to leave two concurrent writes untransformed, which diverges the client holding an unsent write -- it applies the remote value and never re-applies its own, so it ends on a value the server does not hold. The rule is now the one data values already use, and two scenarios cover the same-key and different-key cases. A failing settings provider: the behaviour for a provider that throws or returns late was implemented but unspecified, and nothing covered one that never settles or a close arriving mid-build. Spelled out as a requirement of its own, since what it protects is the menu opening at all. design.md still had ToolbarUI owning the settings button and dispatching ui:block-settings:open, from before the ARIA work was folded in. The specs and the code give the button to BlockSettingsUI; the doc now says so, and says why. Its MenuConfig references follow the rename. The sdk delta's scenarios still said "both" and "either" of what its own requirement already called three maps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot's review of the implementation PRs. Two of its findings are pre-existing OT defects that affect plain data values too, and are on the board rather than here; the rest are specified below. Plugin names and the keys inside an entry are document data, so neither can be allowed to collide with Object.prototype, and what the API hands back has to be detached from the store -- over a live record the no-op rule turns read-modify-write into silent loss. The settings button's contract now says the toggle holds however it is activated, and that the button does not submit a form it sits inside. Two more ways a menu could come apart from the block it was built for: a dismissal while a replacement is being built, and selection moving during a build. Only caret movement is filtered for repeats. Pointer entry was being deduplicated too, which stranded listeners that ignore selection while the menu is open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reversean's follow-ups: the specs were reconciled but the surrounding artifacts were not. plugin-block-data/design.md still said two plugin data operations are left untransformed and that the last applied wins, and pinned the undo limitation on that. The rule is now the revision one; the stale undo entry is dropped rather than replayed, which is checked rather than assumed. The limits that do remain -- the in-flight window on the client and the missing Neutral case on the server -- are stated where they are and pointed at the board card, since both predate this change and affect plain data values too. block-settings-ui/proposal.md and tasks.md still had ToolbarUI rendering the settings button and tracking the hovered block. They now say what the specs and the code say: the plugin renders the button and follows selection, the toolbar mounts it and owns the tab stop. MenuConfig references follow the rename there too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One conflict, in the `ui` capability's note about test coverage: main's #174 rewrote it for the teardown suites, this branch for the block settings suites. Both are true, so the resolution names all four files. #174 also added a "Destroying the blocks UI" scenario inside "Blocks holder rendering and input capture", which this change's delta MODIFIES. The archive fold replaces a modified requirement whole, so the delta would have deleted that scenario on archive; it is carried into the delta instead, with this change's own `selectionchange` listener added to what teardown removes. Checked the same way for every other requirement these deltas modify. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows up on the review in #157. Instead of wiring a
BlockTuneAdapterand aBlockTunesManageraround the Block Tune entity, this proposes removing the entity: a "tune" becomes a plain plugin that stores per-block data in the Model and adds an item to the block settings menu. "Tune" stays as a UI term only.Artifacts only — no implementation in this PR. Three changes, so each lands on its own:
plugin-block-data(36 tasks, no dependencies)BlockNode'stunesstore becomesplugins, keyed by plugin name, created on first write (todayupdateTuneDatacrashes for an absent entry). AddsBlocksAPI.getPluginData/updatePluginData, makes plugin-data changes undoable and collaborative, renames the model's tune vocabulary (PluginDataNode,PluginDataIndexwithk: "plugin",PluginDataModifiedEvent), types the accessors through a new augmentableEditorjsPluginDataMap, and carries v2tunesdata across incomposeDataFromVersion2.Breaking, pre-1.0: the serialized block key
tunes→plugins, and the serialized indexk: "tune"→k: "plugin". Clients andot-serverdeploy together, since the server applies operations against its own model.block-settings-ui(31 tasks)BlockSettingsUI(name: 'block-settings') renders the settings popover and exposesregister(provider), where a provider returns aMenuConfigfor the target block each time the menu opens — the same model asInlineTool#getToolbarConfig.ToolbarUIgrows a settings button, and the built-in Move up / Move down / Delete become@editorjs/block-actions-plugin. Ports the toolbar button, popover wiring and internal-tune behavior from #157 (credited to @bettysteger).Assumes
test/editorjs-e2e-and-aria-openspechas merged, since it builds on that branch'smessages.ts, toolbar ARIA/roving-tabindex behavior, Playwright harness and@editorjs/ui-kit2.x. Changing a block's appearance from a plugin is deliberately deferred to its own change; the analysis is parked in itsdesign.md.remove-block-tunes(15 tasks, no dependencies)Deletes
ToolType.Tune,BlockTune,BlockTuneFacade,ToolsCollection#blockTunes, the tooltunesoption and its facade getters. A registered tune is currently built into a facade that nothing ever reads, so nothing working is lost.Plan: this PR reviews the plan; implementation follows as stacked PRs, starting with
plugin-block-dataandremove-block-tunes. Supersedes #157.🤖 Generated with Claude Code