Repository navigation
feat: A1 · Inspector sidebar (block + document settings) - #349
Conversation
Replaces the M7 placeholder right sidebar with a real tabbed inspector: - `InspectorSidebar` shell with ARIA tabs pattern (Block / Document), arrow-key navigation, focus management, and an empty-state message on the Block tab when nothing is selected. - Block tab renders `@wordpress/block-editor`'s `<BlockInspector />` so core blocks get Color / Typography / Dimensions / Border / Advanced panels for free via block supports. - Document tab with built-in panels: Status & visibility (status, slug, optional author picker), Featured image (consumes the media bridge — will light up automatically when A3 lands), Excerpt, and Discussion (conditional on host opt-in). Panels are independently gated via `EditorAppProps.supports`. - Host extension API: `<PluginDocumentSettingPanel>` slot-fill component *and* the `ap.visual-editor.document-panels` filter; both funnel into a single slot so WP-style React devs and Laravel- style hooks devs get consistent surfaces. - Title moves from the top bar into the canvas (WP-style), slug and status move from the top bar into the Document sidebar panel. Top bar is now save-indicator + undo/redo + inserter/inspector toggles + preview + menu only. - `PostTitle` component renders a focus-visible, auto-grow textarea above the block list. - `@wordpress/format-library` added and imported for its side effect so bold/italic/link/inline-code formats register and the block toolbar's inline formatting controls show up inside RichText. - `settings.typography.fontFamilies.custom` shape provided so the Font family picker renders alongside size / weight / line height. - `supports.color.enableContrastChecker = false` applied globally via a `blocks.registerBlockType` filter — upstream's deps-less `useLayoutEffect` in `BlockColorContrastChecker` was contributing to the color-drag crash. Follow-up #348 re-introduces the warning using `artisanpack-ui/accessibility` without the DOM-measurement loop. - `usePersistence.queueBlocksForSave` splits save-buffering from `setBlocks`. `onInput` (drag frames, mid-typing) now buffers for save without re-rendering the editor tree; `onChange` (persistent commits) still mirrors into React state for undo/redo. - Default autosave debounce bumped 800ms → 1500ms to further reduce backpressure during rapid color-picker input. - StrictMode wrapper dropped from `main.tsx` — v32/15 @wordpress/* internals aren't StrictMode-safe and the dev-mode double-invocation was amplifying the backpressure. - Gutenberg packages pinned to `.43 / 15.16 / 32.5` (down from `.44 / 15.17 / 32.6`). 15.17 shipped a `blockListSettings` store- shape regression that broke the editor with our integration. - Blade component and Vue wrapper emit/pass the new mount props (excerpt, featured image, author, comments-open, author options, supports). Known limitation: fast-drag on the gradient color picker still crashes the block with "Maximum update depth exceeded" inside Gutenberg's block-support hooks. Presets, slow drag, hex input, and numeric RGB/HSL input all work. Follow-up #347 iframes the canvas, which matches WordPress's own architecture and will eliminate the backpressure. Tests: +30 JS tests (inspector sidebar, document panels, extension API, PostTitle) and +2 PHP tests (Blade data-attr emission). Full suite green: 475 JS, 152 PHP. Closes #343 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a tabbed inspector sidebar (Block / Document) with built-in and plugin-extensible document panels (status/slug/author, featured image, excerpt, discussion), moves the post title into the canvas, extends mount/config/Vue/Blade/PHP inputs for document metadata, refactors debounced persistence, updates test setup (matchMedia polyfill) and WordPress deps, and adds tests and styles. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant InspectorSidebar
participant DocumentPanels
participant MediaUpload
participant EditorApp
User->>InspectorSidebar: Open Document tab / click "Set featured image"
InspectorSidebar->>DocumentPanels: Delegate featured-image action
DocumentPanels->>MediaUpload: Open picker (MediaUpload)
MediaUpload-->>DocumentPanels: onSelect(media)
DocumentPanels->>EditorApp: onFeaturedImageChange(validatedMedia)
EditorApp-->>EditorApp: update local state & emit metadata change
EditorApp-->>User: UI updates (preview shown)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
resources/js/visual-editor/editor/top-bar.tsx (1)
155-181:⚠️ Potential issue | 🟠 MajorGlobal undo/redo shortcut handling can hijack native text editing undo.
Line 164 prevents default for
Cmd/Ctrl+Zregardless of focused target. With metadata inputs now in the canvas/sidebar, this can block native undo/redo while typing in form fields/contenteditable areas.Proposed fix
+function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) { + return false; + } + + if (target.isContentEditable) { + return true; + } + + const tag = target.tagName; + return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'; +} + useEffect(() => { function handleShortcut(event: KeyboardEvent): void { if (!isModifier(event)) { return; } const key = event.key.toLowerCase(); if (key === 'z') { - event.preventDefault(); + if (isEditableTarget(event.target)) { + return; + } if (event.shiftKey) { if (canRedo) { + event.preventDefault(); onRedo(); } } else if (canUndo) { + event.preventDefault(); onUndo(); } return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@resources/js/visual-editor/editor/top-bar.tsx` around lines 155 - 181, The global shortcut handler in useEffect's handleShortcut is preventing native undo/redo while typing; update handleShortcut (used with isModifier, onUndo, onRedo, onSave, canUndo, canRedo) to first detect whether event.target is an editable element (e.g., HTMLInputElement, HTMLTextAreaElement, or an element with isContentEditable true) or an element with a role/attribute indicating form input, and if so, do nothing (return) so native browser undo/redo is preserved; only call event.preventDefault() and invoke onUndo/onRedo/onSave when the target is not editable and the corresponding canUndo/canRedo/onSave checks pass.
🧹 Nitpick comments (1)
tests/Feature/VisualEditor/VisualEditorComponentTest.php (1)
152-154: Consider asserting JSON payload content, not just attribute presence.Line 152 through Line 154 only checks that the attributes exist; adding value-level assertions (even partial key/value checks) would better catch serialization regressions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/Feature/VisualEditor/VisualEditorComponentTest.php` around lines 152 - 154, The current assertions in VisualEditorComponentTest (the $html checks using ->and( $html )->toContain('data-featured-image='), 'data-author-options=' and 'data-supports=') only verify attribute existence; update the test to extract each attribute's value from $html (e.g., via regex or a DOM parser), json_decode the attribute string and assert on specific keys/values (for example check that featured-image contains expected keys, author-options includes expected author id or name, and supports contains expected booleans), replacing or augmenting the toContain assertions so serialization regressions are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Around line 73-81: package.json is missing an explicit dependency on
`@wordpress/hooks` which is imported across multiple modules (e.g.,
plugin-document-setting-panel.tsx, editor-app.tsx, media-bridge/state.ts) and
can cause resolution/hoisting issues; add "@wordpress/hooks" to the dependencies
in package.json using a version that matches the major version used by the other
`@wordpress/`* packages in this file (e.g., align with "15.x" or the major used by
`@wordpress/blocks/`@wordpress/block-editor), ensuring the package name
"@wordpress/hooks" is declared so installers resolve a single compatible copy.
In `@resources/js/visual-editor/editor/document-panels.tsx`:
- Line 145: The memo currently freezes filter-registered panels by calling
useMemo(() => getFilteredDocumentPanels(), []) with an empty dependency array;
update this so getFilteredDocumentPanels() is recomputed whenever the panel
filters change (e.g., remove the empty deps to call it on every render or add
the appropriate reactive dependency such as the filter registry/state that backs
ap.visual-editor.document-panels). Ensure the variable filteredPanels continues
to be assigned from getFilteredDocumentPanels() and that useMemo (or no memo)
references the filter registry/state so panels registered after mount appear.
In `@resources/js/visual-editor/editor/editor-app.tsx`:
- Around line 161-199: The preset label strings in DEFAULT_PALETTE,
DEFAULT_FONT_SIZES, and DEFAULT_FONT_FAMILIES are user-facing and must be
localized; wrap each name value (e.g., 'Base content', 'Small', 'System', etc.)
with the translation helper __() just like other UI strings (e.g., name:
__('Base content')). Keep slugs and other non-display fields unchanged, and
ensure the translation import/usage is consistent with the rest of the file.
- Around line 452-470: handleInput currently only calls
queueBlocksForSave(next), leaving the controlled value passed to
BlockEditorProvider stale; add a lightweight transient state update so the
provider's value stays in sync without forcing the heavy re-render cascade.
Specifically, introduce a transientBlocks state (e.g., transientBlocks,
setTransientBlocks) or a shallow-ref-backed state used as the value for
BlockEditorProvider, update setTransientBlocks(next) inside handleInput
alongside queueBlocksForSave(next), and keep the existing onChange handler to
commit to the main blocks state (setBlocks/commitBlocks) and push history;
ensure the transient value is decoupled from the persistent state so
sidebars/inspector-heavy effects are not retriggered on every frame.
In `@resources/js/visual-editor/editor/inspector-sidebar.tsx`:
- Around line 214-246: The current conditional rendering unmounts the inactive
tabpanel causing PanelBody and plugin state to reset and leaving aria-controls
dangling; modify the render logic so both tabpanels (the block panel containing
BlockInspector and the document panel rendering documentContent) are always
mounted using their existing ids blockPanelId and documentPanelId, and toggle
visibility of the inactive panel (e.g., via a hidden class, aria-hidden, or
style display:none) based on activeTab so state is preserved and aria references
remain valid.
In `@resources/js/visual-editor/editor/main.tsx`:
- Around line 133-165: initialAuthorId is left as the raw dataset string which
can turn numeric ids like "1" into strings; normalize it before returning by
locating the initialAuthorId variable and replacing its use with a normalized
value: if initialAuthorId is falsy string treat as undefined; otherwise, if
authorOptions is available, find an AuthorOption in authorOptions where
option.value === initialAuthorId or option.value === Number(initialAuthorId) and
set initialAuthorId to that option.value (preserving its original number|string
type); if no match, convert numeric-looking strings to Number(initialAuthorId)
otherwise keep the string; finally use this normalized initialAuthorId in the
returned object instead of the raw dataset value so onMetadataChange emits the
correct type.
In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 115-139: The returned list from getFilteredDocumentPanels (the
pipeline starting from the valid variable and ending with .map(({ panel }) =>
panel)) can contain duplicate DocumentPanelSpec.id values which will cause
duplicate React keys; after filtering and sorting but before the final .map,
deduplicate by id (use the DocumentPanelSpec.id field) so only one panel per id
remains—implement by iterating the sorted array and inserting into a Map keyed
by panel.id (or use a reducer) so that the chosen policy (e.g., last-wins or
first-wins) is explicit; update the pipeline that uses valid, index, sort, and
final map to return the deduplicated array of panels.
In `@resources/js/visual-editor/editor/post-title.css`:
- Around line 49-50: The CSS rule in
resources/js/visual-editor/editor/post-title.css currently uses the deprecated
property `word-break: break-word`; update that rule (the selector block
containing `overflow: hidden; word-break: break-word;`) to remove `word-break:
break-word` and instead add `overflow-wrap: anywhere` so text wrapping behavior
is preserved and compliant with current CSS specs.
- Around line 63-70: The .screen-reader-text rule uses the deprecated clip
property; replace clip: rect(0, 0, 0, 0); with a modern clip-path-based
visually-hidden pattern (e.g., clip-path: inset(50%); and include a prefixed
fallback like -webkit-clip-path: inset(50%)); keep the other properties
(position, width, height, margin, padding, overflow) so the element remains
accessible and visually hidden; update the .screen-reader-text CSS block
accordingly.
---
Outside diff comments:
In `@resources/js/visual-editor/editor/top-bar.tsx`:
- Around line 155-181: The global shortcut handler in useEffect's handleShortcut
is preventing native undo/redo while typing; update handleShortcut (used with
isModifier, onUndo, onRedo, onSave, canUndo, canRedo) to first detect whether
event.target is an editable element (e.g., HTMLInputElement,
HTMLTextAreaElement, or an element with isContentEditable true) or an element
with a role/attribute indicating form input, and if so, do nothing (return) so
native browser undo/redo is preserved; only call event.preventDefault() and
invoke onUndo/onRedo/onSave when the target is not editable and the
corresponding canUndo/canRedo/onSave checks pass.
---
Nitpick comments:
In `@tests/Feature/VisualEditor/VisualEditorComponentTest.php`:
- Around line 152-154: The current assertions in VisualEditorComponentTest (the
$html checks using ->and( $html )->toContain('data-featured-image='),
'data-author-options=' and 'data-supports=') only verify attribute existence;
update the test to extract each attribute's value from $html (e.g., via regex or
a DOM parser), json_decode the attribute string and assert on specific
keys/values (for example check that featured-image contains expected keys,
author-options includes expected author id or name, and supports contains
expected booleans), replacing or augmenting the toContain assertions so
serialization regressions are caught.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 56016fc9-fc6b-4248-90ba-6c79a5873fd3
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json,!package-lock.json
📒 Files selected for processing (23)
package.jsonresources/js/visual-editor/_legacy/editor/test-setup.tsresources/js/visual-editor/editor/__tests__/document-panels.test.tsxresources/js/visual-editor/editor/__tests__/inspector-sidebar.test.tsxresources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsxresources/js/visual-editor/editor/__tests__/post-title.test.tsxresources/js/visual-editor/editor/__tests__/top-bar.test.tsxresources/js/visual-editor/editor/document-panels.tsxresources/js/visual-editor/editor/editor-app.cssresources/js/visual-editor/editor/editor-app.tsxresources/js/visual-editor/editor/inspector-sidebar.cssresources/js/visual-editor/editor/inspector-sidebar.tsxresources/js/visual-editor/editor/main.tsxresources/js/visual-editor/editor/plugin-document-setting-panel.tsxresources/js/visual-editor/editor/post-title.cssresources/js/visual-editor/editor/post-title.tsxresources/js/visual-editor/editor/top-bar.cssresources/js/visual-editor/editor/top-bar.tsxresources/js/visual-editor/editor/use-persistence.tsresources/js/visual-editor/editor/vue.tsresources/views/components/editor.blade.phpsrc/View/Components/VisualEditorComponent.phptests/Feature/VisualEditor/VisualEditorComponentTest.php
| const handleInput = useCallback( | ||
| (next: BlockInstance[]): void => { | ||
| onBlocksChange(next); | ||
| // `onInput` fires on intermediate, non-persistent changes — | ||
| // color-picker drag frames, mid-typing rich-text updates, etc. | ||
| // Calling `setBlocks(next)` here re-renders the entire editor | ||
| // tree (TopBar, InspectorSidebar, BlockInspector panels) on | ||
| // every frame, which triggers a runaway layout-effect cascade | ||
| // inside Gutenberg's block-support hooks and crashes the | ||
| // block with "Maximum update depth exceeded" (#343 A1). | ||
| // | ||
| // The block-editor store already has the latest tree from | ||
| // the dispatch that triggered this callback, so the canvas | ||
| // stays in sync without our mirror. We only need to queue a | ||
| // debounced save — undo/redo still works because `onChange` | ||
| // (fired on the persistent commit at the end of the drag) | ||
| // does the React state update and history push. | ||
| queueBlocksForSave(next); | ||
| }, | ||
| [onBlocksChange] | ||
| [queueBlocksForSave] |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For @wordpress/block-editor's BlockEditorProvider, is the valueprop a controlled value that should stay synchronized from bothonInputandonChange callbacks? Please cite Gutenberg docs or source/examples.
💡 Result:
Yes. BlockEditorProvider’s value prop is a controlled value (an array of blocks) and it is expected to be kept in sync with whatever state you manage externally. The provider is implemented as a controlled input: it “expected to receive a value of a blocks array” and it “calls onChange and/or onInput when the user interacts to change blocks”, so your callbacks must update the value you pass back in. Regarding whether you need to synchronize updates from both callbacks: the docs distinguish them as “persistent” (onChange) vs “non-persistent” (onInput) changes; however, they also state “While value updates should always be reflected immediately (onInput), you may only want history entries to reflect change milestones (onChange).” So the general pattern is: update the value immediately from onInput, and optionally treat onChange differently (e.g., persistence/history), but you should ensure the authoritative value you pass back stays synchronized with what the editor is doing. Authoritative sources: - BlockEditorProvider package docs (provider README) describe it as a controlled input with value being the current blocks array, and that it calls onChange/onInput on user interaction. - The same README explains the expected sync semantics: updates should be reflected immediately via onInput, while onChange is for persistent milestones. - The @wordpress/block-editor package usage docs provide examples where onInput and onChange both update the external blocks state that is then fed back into value. Example (from docs; both callbacks update the same state): const [ blocks, updateBlocks ] = useState( [] ); return ( <BlockEditorProvider value={ blocks } onInput={ ( nextBlocks ) => updateBlocks( nextBlocks ) } onChange={ ( nextBlocks ) => updateBlocks( nextBlocks ) } > ... );
Citations:
- 1: https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md
- 2: https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/README.md
- 3: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/
- 4:
BlockEditorProviderdocumentation out of date WordPress/gutenberg#25333
BlockEditorProvider's controlled value must stay in sync with onInput changes.
Per Gutenberg's BlockEditorProvider documentation, value is a controlled prop that should be updated immediately from onInput callbacks. The current code at line 468 skips the state update (calling only queueBlocksForSave) to avoid rerender cascades, but this leaves the value passed to BlockEditorProvider stale during transient edits. If the shell re-renders before onChange commits, it can replay a stale block tree into the provider and silently drop in-progress typing or drag state.
The decorator's comment correctly identifies the rerender cost problem, but the solution must sync the controlled value without triggering cascades—likely by decoupling the save queue from the editor state update or using a different synchronization strategy.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@resources/js/visual-editor/editor/editor-app.tsx` around lines 452 - 470,
handleInput currently only calls queueBlocksForSave(next), leaving the
controlled value passed to BlockEditorProvider stale; add a lightweight
transient state update so the provider's value stays in sync without forcing the
heavy re-render cascade. Specifically, introduce a transientBlocks state (e.g.,
transientBlocks, setTransientBlocks) or a shallow-ref-backed state used as the
value for BlockEditorProvider, update setTransientBlocks(next) inside
handleInput alongside queueBlocksForSave(next), and keep the existing onChange
handler to commit to the main blocks state (setBlocks/commitBlocks) and push
history; ensure the transient value is decoupled from the persistent state so
sidebars/inspector-heavy effects are not retriggered on every frame.
- Declare `@wordpress/hooks@4.43.0` explicitly in package.json — we import `addFilter` / `applyFilters` across editor-app, media-bridge, and the extension API, and relying on transitive resolution was fragile (got picked up through multiple nested copies). - Remove `useMemo([])` around `getFilteredDocumentPanels()` — `@wordpress/hooks` filters are global module state with no React- level subscription, so memoizing froze the panel list at mount and hid any panels a host registers after that point. Now recomputed each render (one cheap `applyFilters` call). - Wrap `DEFAULT_PALETTE`, `DEFAULT_FONT_SIZES`, and `DEFAULT_FONT_FAMILIES` name labels in `__()` so the pot-extraction tooling picks them up. - Keep both inspector tabpanels mounted; toggle `hidden` on the inactive one so `PanelBody` open-state, filter-registered plugin state, and ARIA references survive tab switches. - Normalize `initialAuthorId` from the Blade dataset: match against `authorOptions` first (preserving the host-declared number|string type), fall back to numeric coercion when the string looks numeric, otherwise leave as a string. `onMetadataChange` now emits the correct type for Laravel hosts that store integer author ids. - Deduplicate filter-registered panels by id with a last-wins policy, matching how `@wordpress/hooks` itself composes filters. - Swap deprecated CSS: `word-break: break-word` → `overflow-wrap: anywhere`; `clip: rect(...)` → `clip-path: inset(50%)` with `-webkit-clip-path` for older Safari. - Add a rationale comment to `handleInput` explaining why letting the controlled `value` go stale between commits is intentional. Passing `setTransientBlocks(next)` on every drag frame — as one review suggestion proposed — would reintroduce the render cascade that causes "Maximum update depth exceeded" during color drag; there's no React pattern that feeds a reactive `value` prop without triggering a render, and the block-editor store already keeps itself in sync, so the mirror isn't needed. Tests: +2 for tabpanel `hidden` toggling, +1 for panel dedupe, +6 for `normalizeAuthorId`. Full suite: 483 JS / 152 PHP, all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@resources/js/visual-editor/editor/main.tsx`:
- Around line 142-157: The parsed authorOptions value from parseJsonDataset may
be valid JSON but not an array; before calling normalizeAuthorId or using .find,
validate that authorOptions is an actual array (e.g.
Array.isArray(authorOptions)) and if not treat it as null (or an empty array) so
downstream code like normalizeAuthorId(rawAuthorId, authorOptions) and any .find
calls won’t throw; update the initialization around authorOptions and the
use-site for initialAuthorId to pass the safe/normalized value instead of the
raw parsed result.
In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 141-152: The dedup logic using `deduped.set(panel.id, panel)`
doesn't move an existing key to its new sorted position because Map#set
preserves insertion order; update the loop in plugin-document-setting-panel.tsx
to, for each `panel` in `sorted`, if `deduped.has(panel.id)` first
`deduped.delete(panel.id)` then `deduped.set(panel.id, panel)` so the later
duplicate is reinserted at the correct position; keep returning
`Array.from(deduped.values())`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3cd231c3-4828-4c3e-b4e2-c0f26527c6dc
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json,!package-lock.json
📒 Files selected for processing (10)
package.jsonresources/js/visual-editor/editor/__tests__/inspector-sidebar.test.tsxresources/js/visual-editor/editor/__tests__/main.test.tsresources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsxresources/js/visual-editor/editor/document-panels.tsxresources/js/visual-editor/editor/editor-app.tsxresources/js/visual-editor/editor/inspector-sidebar.tsxresources/js/visual-editor/editor/main.tsxresources/js/visual-editor/editor/plugin-document-setting-panel.tsxresources/js/visual-editor/editor/post-title.css
✅ Files skipped from review due to trivial changes (5)
- package.json
- resources/js/visual-editor/editor/post-title.css
- resources/js/visual-editor/editor/inspector-sidebar.tsx
- resources/js/visual-editor/editor/tests/inspector-sidebar.test.tsx
- resources/js/visual-editor/editor/document-panels.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- resources/js/visual-editor/editor/tests/plugin-document-setting-panel.test.tsx
- resources/js/visual-editor/editor/editor-app.tsx
- `readMountConfig` now guards `authorOptions` with `Array.isArray` — if a host mis-emits `data-author-options` as a non-array value (object, primitive, etc.), we treat it as null instead of crashing `normalizeAuthorId`'s `.find` or the SelectControl props builder downstream. - `getFilteredDocumentPanels` dedupe now deletes-before-re-inserting when it sees a duplicate id. `Map#set` on an existing key updates the value but preserves the original insertion position; since our last-wins policy should also move the panel to its *later* position, the delete-then-set sequence is required for ordering to reflect the most recent registration. Tests updated to assert the positional part of dedupe (`['unique', 'shared']`, not just `length === 2`) and that `normalizeAuthorId` tolerates the null fallback. Full suite: 484 JS / 152 PHP, all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
resources/js/visual-editor/editor/plugin-document-setting-panel.tsx (1)
26-27: Consider consolidating imports from the same package.Both imports are from
@wordpress/componentsand could be combined into a single statement.♻️ Suggested consolidation
-import { PanelBody } from '@wordpress/components'; -import { createSlotFill } from '@wordpress/components'; +import { PanelBody, createSlotFill } from '@wordpress/components';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx` around lines 26 - 27, Consolidate the two separate imports from `@wordpress/components` into a single import statement: replace the two lines importing PanelBody and createSlotFill with one line importing both (e.g., import { PanelBody, createSlotFill } from '@wordpress/components';) and remove the duplicate import; this change affects the top-level imports in plugin-document-setting-panel.tsx and requires no further code changes if PanelBody and createSlotFill are only used by name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@resources/js/visual-editor/editor/plugin-document-setting-panel.tsx`:
- Around line 26-27: Consolidate the two separate imports from
`@wordpress/components` into a single import statement: replace the two lines
importing PanelBody and createSlotFill with one line importing both (e.g.,
import { PanelBody, createSlotFill } from '@wordpress/components';) and remove
the duplicate import; this change affects the top-level imports in
plugin-document-setting-panel.tsx and requires no further code changes if
PanelBody and createSlotFill are only used by name.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 260a6f86-4221-4450-a00f-9e9314514a60
📒 Files selected for processing (4)
resources/js/visual-editor/editor/__tests__/main.test.tsresources/js/visual-editor/editor/__tests__/plugin-document-setting-panel.test.tsxresources/js/visual-editor/editor/main.tsxresources/js/visual-editor/editor/plugin-document-setting-panel.tsx
✅ Files skipped from review due to trivial changes (1)
- resources/js/visual-editor/editor/tests/plugin-document-setting-panel.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- resources/js/visual-editor/editor/tests/main.test.ts
- resources/js/visual-editor/editor/main.tsx
Description
Replaces the M7 right-sidebar placeholder (#343) with a real tabbed inspector. The Block tab renders
@wordpress/block-editor's<BlockInspector />so core blocks get Color / Typography / Dimensions / Border / Advanced panels for free. The Document tab adds built-in Status & visibility, Featured image, Excerpt, and (opt-in) Discussion panels, plus two host-extension surfaces so developers can plug in their own panels.Title has moved into the canvas (WordPress-style); slug and status have moved into the Document panel. The top bar keeps only save indicator + undo/redo + inserter/inspector toggles + preview + more-options menu.
Closes: #343
Type of Change
Related Issue
Issue: #343
Follow-ups filed (not blocking this PR):
artisanpack-ui/accessibilityMotivation and Context
V1 expansion plan (
docs/plans/11-v1-expansion.md, Phase A) requires the inspector sidebar to ship with real block + document settings before 1.0.0. M7 (#317) left both sidebars as literal "lands in a later milestone" placeholder strings — not a shippable CMS experience. A1 delivers the block-settings half of that work.Changes Made
New components
InspectorSidebar— ARIA-compliant tabbed sidebar shell. Block and Document tabs always visible; arrow-key navigation; focus moves to the active tab on mount; auto-switches to Block when a block gains selection; Block tab shows "Click on a block to view its settings." empty state when nothing is selected.DocumentPanels— built-in panels:authorOptions)editor.MediaUploadslot-fill; will light up automatically when A3 (A3 · Media library integration #345) replaces the bridge stubsupports.excerpt !== falsesupports.comments === true(default off, since host model compatibility varies)PluginDocumentSettingPanel+ap.visual-editor.document-panelsfilter — two ways for host apps to contribute custom panels, both funnel into one slot. Exported from the package main.PostTitle— large, auto-growing textarea rendered at the top of the canvas above the block list. Keyboard focus indicator passes WCAG contrast.Changed
TopBarstrips title/slug/status inputs and their callbacks; CSS and tests updated accordingly.EditorAppwires everything; new mount propsinitialExcerpt,initialFeaturedImage,initialAuthorId,initialCommentsOpen,authorOptions,supports.usePersistencesplits save-buffering from state update.queueBlocksForSave(next)schedules a debounced save without callingsetBlocks.onInputnow uses that path so drag frames don't re-render the editor tree;onChangestill mirrors for undo/redo.<x-visual-editor>component +VisualEditorComponentPHP class emit the new mount data attributes (JSON-encoded for arrays/objects).@wordpress/*v32/15 components aren't StrictMode-safe in dev mode and the double-invocation amplifies backpressure during drag-heavy interactions.Block-support tuning
@wordpress/format-libraryimported for its side effect — registers core rich-text formats (bold, italic, link, inline code, etc.) so the block toolbar's inline formatting controls actually render inside RichText blocks.settings.typography.fontFamilies.customshape provided so the Font family picker appears in the Typography panel.settings.typography.textAlign: trueenables the toolbar text-alignment controls on paragraph/heading.__experimentalFeaturesshape — color (text/background, custom), typography (full set),alignWide. Layout/border/spacing/customGradient deliberately left off for this pass to reduce the surface area of the color-drag backpressure (they can be reintroduced alongside A1 follow-up · Iframe the editor canvas (fixes color-picker fast-drag crash) #347).supports.color.enableContrastChecker = falseapplied globally via ablocks.registerBlockTypefilter. Upstream's deps-lessuseLayoutEffectinBlockColorContrastCheckeris part of the backpressure picture; follow-up A1 follow-up · Restore contrast warning via artisanpack-ui/accessibility #348 reimplements contrast warning against theartisanpack-ui/accessibilitypackage without DOM-measurement.Gutenberg pin changes
Pinned the following down one minor line:
@wordpress/block-editor: 15.17.0 → 15.16.0@wordpress/block-library: 9.44.0 → 9.43.0@wordpress/blocks: 15.17.0 → 15.16.0@wordpress/components: 32.6.0 → 32.5.0@wordpress/core-data: 7.44.0 → 7.43.0@wordpress/format-library: (new) → 5.43.0@wordpress/patterns: 2.44.0 → 2.43.0@wordpress/reusable-blocks: 5.44.0 → 5.43.015.17 shipped a
state.blockListSettingsstore-shape regression that broke the editor when our integration tried to register blocks; 15.16 is stable.How Has This Been Tested?
Testing Environment:
Tests Performed:
onMetadataChange./visual-editor/api/...with the updated block tree.Known limitation (see #347): fast-drag on the gradient color picker crashes the block with "Maximum update depth exceeded" inside Gutenberg's block-support render pipeline. Presets, slow drag, hex input, numeric RGB/HSL input all work. The fix is iframing the canvas (which is what WordPress itself does) — tracked as #347.
Accessibility Tests Run
aria-label,aria-selected,aria-controls,role="tab"/"tabpanel"/"complementary"all correctly wired--color-primary; PostTitle focus ring uses same. DaisyUI tokens are contrast-tested at the host level__()with TEXT_DOMAINDetails: Contrast warning on text/background combinations is temporarily off (filter disables the upstream checker) — tracked as #348 for re-introduction via
artisanpack-ui/accessibility.Tests Added
Test details:
inspector-sidebar.test.tsx(10 tests) — tab visibility, selection auto-switch, empty-state, arrow-key focus, tab click handling, initial focusdocument-panels.test.tsx(13 tests) — status/slug/author change flows, support gates (excerpt/featuredImage/comments), featured-imageonSelectflow, filter-registered panels, author-option type preservationplugin-document-setting-panel.test.tsx(5 tests) — filter ordering with stable sort, malformed entry filtering, slot-fill renderpost-title.test.tsx(4 tests) — render, placeholder handling, onChange wiringVisualEditorComponentTest.php(+2) — document-panel data-attr emission, omission when props unsetDocumentation
Documentation details: The
PluginDocumentSettingPanel,DOCUMENT_PANELS_FILTER, andDocumentPanelSpecexports are fully JSDoc'd with usage guidance for both slot-fill and hooks-based extension patterns.Pre-Submission Checklist
--base release/1.0returned "No findings" after addressing the initial PostTitle focus-visible recommendation)Summary by CodeRabbit
New Features
Style
Tests
Chores