Skip to content

feat: Add editor content engine with visual editor - #820

Merged
aXenDeveloper merged 2 commits into
canaryfrom
editor/content-engine-visual-editor
Sep 20, 2026
Merged

aXenDeveloper merged 2 commits into
canaryfrom
editor/content-engine-visual-editor

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Improving Documentation

Description

What?

Why?

@vercel

vercel Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
vitnode-api Building Building Preview Sep 20, 2026 7:11am UTC
vitnode-api-1789888259251-CPRA Canceled Canceled Sep 20, 2026 7:11am UTC
vitnode-prod Ready Ready Preview Sep 20, 2026 7:11am UTC

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6248fad0bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +79 to +82
const [base] = useState<BlockUnknownData>(() => instance.data);
const [specs] = useState(() => blockFieldSpecs(entry.definition));
const [formSchema] = useState(() =>
buildFormSchemaFromSpec(blockFormSpec(entry), base),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset the properties form when discarding edits

When the selected block's data changes outside this form—for example after clicking Discard or after a save returns normalized canonical data—the instance prop updates, but base, the schema defaults, and AutoForm's values remain frozen from the original mount because the form is keyed only by the block identity. The controls therefore continue showing the discarded/stale values, and the next valid field change dispatches the entire stale form, reintroducing values the user explicitly discarded or overwriting the server's canonical values. Synchronize or remount the form when its baseline data changes.

Useful? React with 👍 / 👎.

);
}

if (max !== undefined && shipped.length > max) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count area children when validating default bounds

For a default containing Areas, shipped.length counts each Area as one node, while the save validator counts the blocks inside Areas and does not count the Area itself. Thus one Area with two children passes a zone with max: 1, and an empty Area can appear to satisfy min: 1; the page then serves a shipped default that the first unchanged save rejects. Compute the bounds from the same flattened block count used by zodBlockInstances.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the editor/content-engine-visual-editor branch from 6248fad to 25a85fc Compare September 18, 2026 20:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25a85fcecf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{ contentTypeId: id },
);
}
if (min !== undefined && max !== undefined && min > max) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate min against the effective default maximum

When a caller declares field.blocks({ min: 201 }) without an explicit max, this guard is skipped because max is undefined. zodBlockInstances later applies its default maximum of 200, so every possible value violates either min or max and the content type can never create a row. Compare min against max ?? CONTENT_BLOCKS_DEFAULT_MAX and reject the unsatisfiable declaration here.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the editor/content-engine-visual-editor branch from 25a85fc to b5b3b78 Compare September 18, 2026 21:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5b3b78816

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (fieldValue.kind === "blocks") {
const { allowed, max, min } = fieldValue;

if (max !== undefined && (max <= 0 || max > CONTENT_BLOCKS_ABSOLUTE_MAX)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-finite and fractional block bounds

Reject NaN and non-integer bounds here, as the current comparisons accept values such as field.blocks({ max: Number.NaN }) or max: 1.5. zodBlockInstances later enforces the limit using ordinary numeric comparisons, so NaN silently disables the maximum while fractional values produce undocumented effective limits; the corresponding min guard has the same problem. Validate both bounds with Number.isInteger/Number.isFinite, as the editable-page declaration already does.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the editor/content-engine-visual-editor branch from b5b3b78 to 04e1337 Compare September 18, 2026 21:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04e1337ac3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +62 to +64
const parsed = field?.safeParse(values[name]);

return parsed?.success ? [[name, parsed.data] as const] : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow optional date fields to be cleared

When a block declares a non-nullable optional field.dateTime(), clearing an existing value makes AutoFormDateTime emit null, but the optional ISO-date schema rejects null here. The field is consequently omitted from the patch, no update is dispatched, and the old timestamp remains in the block despite the control appearing cleared. Handle the cleared state as a property deletion (or normalize it to undefined) so optional dates can be removed.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eca4bbc7c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vitnode/src/blocks/zone.tsx Outdated
Comment on lines +42 to +44
assertContentZoneId(id);

const declared = blocks === undefined ? page?.resolveZone(id) : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the page allowlist when overriding zone blocks

When a ContentZone inside an EditablePage supplies an explicit blocks value but omits allowedBlocks, this condition prevents the page definition from being resolved at all, so allowed becomes undefined instead of inheriting the zone's declared allowlist. In edit mode the catalog and drop checks consequently permit every registered block, only for the page-layout API to reject the eventual save; in view mode the same override also bypasses the page's allowlist validation. Resolve the declared zone independently and override its blocks and allowlist separately.

Useful? React with 👍 / 👎.

Comment thread packages/vitnode/src/blocks/page.tsx Outdated
Comment on lines +86 to +89
return {
allowedBlocks: zone.allowed,
blocks: Object.hasOwn(zones, zoneId) ? zones[zoneId] : zone.default,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate editable-zone bounds into editor state

For pages that declare min or max, the context passes only the allowlist and blocks into ContentZone, so the editor never learns either bound. A user can therefore delete below min or add/move/duplicate blocks beyond max; the UI continues to enable Save, but the page-layout API then returns 400 and the user must manually undo work. Include these bounds in the mount/state capabilities and enforce them across editor mutations.

Useful? React with 👍 / 👎.

Comment on lines +180 to +190
const pending = insertTarget?.zoneId === zoneId ? insertTarget : null;
const areaId =
request.areaId === undefined
? (pending?.areaId ?? null)
: request.areaId;
const container: EditorContainerRef = { areaId, zoneId };
const nodes = containerNodes(state, container);
if (nodes === null) return;

const instance = createBlockInstanceFor(entry);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop insertions when an area reaches its child limit

When the selected insertion target is an area that already has 50 children, this path still creates and inserts another block, and the drag path has the same missing capacity check. zodBlockInstances caps every area at AREA_CHILDREN_DEFAULT_MAX, so the resulting document can never be saved and the server rejects it after the editor has allowed the operation. Check the target container's capacity before both click insertion and drop/move resolution.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8707919ff7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +73 to +76
{panel === "blocks" ? (
<AvailableBlocksPanel />
) : (
<EditorPropertiesPanel />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve sidebar panel state with Activity

Switching from the block catalogue to a selected block conditionally unmounts AvailableBlocksPanel, so returning to the catalogue loses its search query, scroll position, and other internal UI state; entering Preview unmounts it as well. Keep the alternate panels mounted with React <Activity> so ordinary editor navigation restores the user's catalogue state as required by the repository convention.

AGENTS.md reference: AGENTS.md:L16-L23

Useful? React with 👍 / 👎.

Comment on lines +44 to +47
const EMPTY_UNSETS: ReadonlySet<ContentFormFieldSpec["kind"]> = new Set([
"dateTime",
"slug",
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delete optional text values when cleared

When a block has a non-nullable optional text or textarea field with minLength > 0, clearing a previously stored value produces "", but these kinds are absent from EMPTY_UNSETS. The field schema rejects the empty string, BlockDataSync therefore dispatches no patch, and the old value remains stored while the control appears blank. Treat a cleared optional text control as deletion just like the optional date case.

Useful? React with 👍 / 👎.

value: unknown,
seen: ReadonlySet<string>,
): BlockAreaInstance | null => {
if (!isBlockAreaInstance(value) || seen.has(value.id)) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify oversized areas as invalid

For legacy or externally supplied content, an area with more than AREA_CHILDREN_DEFAULT_MAX children passes readableArea and is mounted as editable content. The reducer merely prevents further growth, and the unsafe-save gate does not check the existing child count, so editing anything else leaves Save enabled even though zodBlockInstances will reject the area. Reject oversized areas here so they enter the explicit invalid-entry workflow instead.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ee63a1fd2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +292 to +295
return (
zone !== undefined &&
(zone.invalid.length > 0 || holdsRejectedBlock(zone))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat out-of-bounds mounted zones as unsafe

When existing content violates a newly tightened min or max—or a generic ContentZone is mounted with already out-of-bounds blocks—this predicate still considers the zone safe because it checks only malformed entries and rejected blocks. The current version now propagates min/max into EditorZoneState and prevents mutations from worsening those bounds, but editing an unrelated block property still enables Save, after which zodBlockInstances rejects the unchanged block count and the user cannot persist the repair-independent edit. Include the mounted zone's block count and configured bounds in the unsafe-save check.

Useful? React with 👍 / 👎.

Comment on lines +70 to +75
onCheckedChange={checked => {
field.onChange(
checked
? Object.fromEntries(leaves.map(leaf => [leaf.name, null]))
: null,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore valid defaults when enabling nullable groups

When a nullable group contains any required or otherwise non-nullable leaf, turning the group back on initializes every leaf to null. The group's Zod schema rejects that object, so BlockDataSync dispatches no patch: the UI appears enabled while the stored group remains null, and saving without manually repairing every invalid leaf loses the toggle after reload. Initialize enabled groups from valid leaf defaults/current values rather than assigning null indiscriminately.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b0085f4e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +30 to +33
const saveBody = z.object({
pageId: z.string().min(1).max(EDITABLE_PAGE_ID_MAX_LENGTH),
zones: zodZones,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the layout version when saving

When two moderators load the same page and edit the same zone, this request carries no updatedAt or other expected version. The advisory lock only serializes the writes; after the first save commits, the second transaction reads that new row and then replaces the zone with its stale snapshot, silently discarding the first moderator's work. Include the version returned by GET in the save contract and reject or merge stale submissions.

Useful? React with 👍 / 👎.

Comment on lines +802 to +803
const nodes = insertAt(zone.nodes, action.index, action.area);
if (!zoneKeepsBounds(zone, nodes)) return state;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the top-level node cap when inserting areas

An area contributes zero to zoneKeepsBounds, so repeated area insertion can grow zone.nodes beyond CONTENT_BLOCKS_ABSOLUTE_MAX while this reducer continues accepting every action. The editor then considers the zone safe and enables Save, but zodContentNodes rejects the payload because its top-level array exceeds the absolute cap. Check the resulting top-level node count here, and in other operations that can grow or unwrap the root list.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 124c1c2cd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +74 to +78
.flatMap(name => {
const field = formSchema.shape[name] as undefined | z.ZodType;
const parsed = field?.safeParse(values[name]);

return parsed?.success ? [[name, parsed.data] as const] : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delete cleared optional leaves inside groups

When an optional non-nullable group leaf is cleared—for example field.dateTime() or text with minLength > 0—this parses the whole group with the cleared leaf still present as null or "". The group schema rejects that value, so no patch is dispatched and the previous stored value remains even though the control appears empty and the editor is not marked dirty. Normalize cleared optional leaves by deleting their nested keys before validating and dispatching the group.

Useful? React with 👍 / 👎.

Comment on lines +288 to +290
setLastSave(payload);

return zodLayout.parse(await response.json());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Adopt the returned layout timestamp after saving

After every successful save, the parsed response is returned only to EditablePage, which adopts its zones internally; SettingsScreen never updates its layout prop. Consequently the header continues to say that nobody has rearranged the page after the first save, or displays the previous updatedAt, until a reload. Store the canonical response in local layout state as well as returning it to the editor.

Useful? React with 👍 / 👎.

<p className="text-sm leading-relaxed text-pretty">
{entry ? t("invalid_block") : t("block.issue.unknown_type")}
</p>
<p className="text-xs leading-relaxed opacity-80">{issue}</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the invalid-block diagnostic at least 14px

The block validation detail is important body copy, but text-xs renders it below the repository's 14px minimum and makes the repair guidance unnecessarily difficult to read. Use text-sm or another size of at least 14px.

AGENTS.md reference: AGENTS.md:L71-L72

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6b4d8a17b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +104 to +108
return {
component: component as BlockComponent,
defaultVariant,
description,
fields: assertBlockFields(id, fields),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsatisfiable block field constraints

When a plugin defines a block field with contradictory constraints, such as field.number({ integer: false, min: 10, max: 1 }) or text whose minLength exceeds maxLength, assertBlockFields checks only the field kind and localization, so the definition and registry still load. createBlockInstanceFor then inserts data that fails the derived schema, and in these examples no value can ever satisfy it, leaving the editor's zone permanently unsafe and unsaveable. Validate the supported fields' shared constraints when defineBlock is called, as content-type definitions already do.

Useful? React with 👍 / 👎.

Comment on lines +77 to +78
max: bounds.max,
min: bounds.min,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the default maximum to standalone zones

When a standalone <ContentZone> is edited without an explicit max, bounds.max remains undefined and the reducer treats the zone as having no block-count ceiling. This is the normal documented pairing with a field.blocks() value, whose write schema defaults to 200 blocks, so the editor can build a zone with more than 200 blocks and only discover on Save that the API rejects it. Pass CONTENT_BLOCKS_DEFAULT_MAX when neither the page nor the call site declares a maximum.

Useful? React with 👍 / 👎.

Comment on lines +88 to +91
const min = narrowed(declared?.min, explicit.min, Math.max);
const max = narrowed(declared?.max, explicit.max, Math.min);

if (min !== undefined && max !== undefined && min > max) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate standalone zone bounds before mounting

For a standalone zone whose bounds come from computed configuration, values such as max={Number.NaN}, a fractional maximum, or a negative minimum pass through because this only compares min > max. In particular, NaN makes every fitsZoneMax/fitsZoneMin comparison fail, so even an otherwise valid mounted zone is marked unsafe and Save stays disabled without identifying the bad declaration; fractional values also create undocumented effective limits. Reject non-finite, non-integer, and out-of-range bounds here, consistently with editable-page and blocks-field declarations.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 484e051f86

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fallback = ({ instance, reason }) =>
createElement(DevNotice, { instance, reason }),
registry,
validate = "development",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate stored block shapes in production

When a plugin changes a block definition after records have already been stored—for example by renaming a required field—the default "development" mode disables the structural check in production, so the stale data is passed directly to the updated component. Components that use the required value as its declared type can then throw and take down the public page; use the structural check by default in production, or otherwise guarantee/migrate stored data before rendering.

Useful? React with 👍 / 👎.

Comment on lines +563 to +568
if (
!incomingChanged ||
zoneChanged(zone) ||
wasSuperseded(zone, next.nodes)
) {
return synced === zone ? state : withZones(state, { [next.id]: synced });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain incoming content for Discard

When a dirty zone receives genuinely new blocks from a refetch or router invalidation, this branch preserves the local edit but drops the incoming snapshot entirely. If the user then clicks Discard, the reducer restores the old zone.initial; the mount effect does not run again because its input has not changed, so the editor remains on stale content and a subsequent edit/save uses an outdated baseline, producing a conflict against the core adapter. Preserve the pending incoming snapshot and adopt it when the local edits are discarded.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68d550fcda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +589 to +591
const zone = state.zones[next.id];

if (zone) return syncMountedZone(state, zone, next);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check zone ids as own properties before mounting

A valid zone id may be "constructor", but on the first mount state.zones[next.id] resolves the inherited Object.prototype.constructor rather than undefined. The truthy value is passed to syncMountedZone as though it were an EditorZoneState, causing the editor to crash while reading its state fields. Use an own-property check or a null-prototype map before treating an id as already mounted.

Useful? React with 👍 / 👎.

Comment on lines +841 to +844
const next = boundedUpdate(zone, found.container, nodes =>
nodes.filter((_, at) => at !== found.index),
);
if (!next) return state;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow rejected blocks to be removed while repairing bounds

When a zone has exact bounds such as min: 1, max: 1 and its sole stored block becomes unknown or disallowed after a plugin or allowlist change, Save is unsafe but this removal path is rejected for falling below min; inserting a replacement is also rejected at max, and the unknown block has no editable properties. The zone is therefore impossible to repair in the editor. Permit removal of rejected blocks while keeping Save disabled until the minimum is restored, or provide an atomic replacement operation.

Useful? React with 👍 / 👎.

Comment on lines +85 to +89
if (min !== undefined && max !== undefined && min > max) {
return `Field "${name}" has min ${min} greater than max ${max}.`;
}

if (defaultValue === undefined) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject integer ranges containing no integer

The new shared validator catches min > max, but a required block field such as field.number({ integer: true, min: 0.1, max: 0.9, required: true }) still returns no issue here. createBlockInstanceFor then seeds the value 1, which the derived Zod schema rejects, and no integer can ever satisfy that schema, leaving every inserted instance permanently unsafe. Reject integer ranges when Math.ceil(min) > Math.floor(max).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86dff5ad84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

savePageLayout,
} from "./page-layout-service";

const zodZones = z.record(z.string(), z.array(zodContentNode));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let layout reads carry malformed entries for repair

When a stored zone contains a legacy or corrupt non-node value, readPageLayout deliberately accepts the array and the editor can classify and remove the bad entry, but this shared response schema still requires every item to satisfy zodContentNode. The shipped zones page likewise parses the GET response with that schema, so a value such as "oops" throws in the loader before the invalid-entry UI can mount, making the documented repair path unreachable. Use a permissive unknown[] schema/type for read payloads while keeping PUT input and successful save responses strict.

Useful? React with 👍 / 👎.

onInput: handleInput,
onKeyDown: handleKeyDown,
onPaste: handlePaste,
ref: setElement,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compose the wrapped field element's ref

When a block wraps a native element that already has an object or callback ref in BlockField, injecting ref: setElement replaces that ref during edit mode. The block then observes null and any focus, measurement, or observer logic using the element stops working until edit mode ends. Compose the child's existing ref with setElement instead of overwriting it.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 511502fe8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +83 to +86
for (const [leaf, leafValue] of Object.entries(
contentInnerFields(fieldValue),
)) {
assertBlockField(blockId, `${name}.${leaf}`, leafValue);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nested groups instead of stopping after one level

When a block declares a group inside another group, this loop validates the inner group descriptor but never traverses its fields. A disallowed descriptor such as field.file() at the next depth therefore passes defineBlock, allowing file IDs to be stored in the block JSON without the foreign-key lifecycle that blockFieldKindRefusal is intended to protect; the shallow renderer check also never examines those grandchildren. Reject group-valued leaves, since groups are documented as non-nesting, rather than accepting an unchecked subtree.

Useful? React with 👍 / 👎.

explicit: BlockAllowedSpec | undefined;
id: string;
}): BlockAllowedSpec | undefined => {
if (declared === undefined) return explicit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate standalone zone allowlists before returning them

When a standalone ContentZone receives an empty or malformed runtime allowlist such as ["core/text"], this early return bypasses all validation. Every registered block then fails isBlockAllowed, so existing blocks are classified as rejected and the editor offers no insertable blocks, leaving the zone unusable without explaining the bad declaration. Validate entries and reject empty lists here, consistently with blocks fields and editable-page zones.

Useful? React with 👍 / 👎.

@aXenDeveloper aXenDeveloper changed the title Editor/content engine visual editor feat: Add editor content engine with visual editor Sep 19, 2026
@github-actions github-actions Bot added the 💡 Feature A new feature label Sep 19, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d32c5b2f06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}) => {
const [editing, setEditing] = useState(canEdit && openEditing);
const [showSidebar, setShowSidebar] = useState(true);
const [layout, setLayout] = useState(loadedLayout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize layout state after loader refreshes

When this route's loader reruns without remounting the page, loadedLayout receives the new server snapshot but this state remains frozen at its initial value. EditablePage therefore never sees the incoming layout, so another moderator's changes remain invisible and starting another edit uses the stale baseline until a full remount or reload. Synchronize this local state when loadedLayout changes; the editor reducer already knows how to preserve dirty local edits while holding an incoming snapshot.

Useful? React with 👍 / 👎.

)}
ref={setDropRef}
>
{area.children.length === 0 ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the add-block action available in populated areas

Once an area contains its first block, this branch renders only children, removing the sole control that calls setInsertTarget with the area's id. Users can still drag blocks into the area, but they can no longer target that area and click a catalog entry to append another block, despite that insertion workflow being available while the area is empty. Render an add action for non-full populated areas as well.

Useful? React with 👍 / 👎.

Comment on lines +168 to +170
if (!containerAcceptsBlock(state, { areaId: null, zoneId: candidate })) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Evaluate insertion capacity on the requested container

When a zone has reached the 1,000-node root cap but one of its areas still has room, zoneAccepts rejects the request by testing the root container first. Consequently both a targeted catalog click and a catalog drag resolved into that area are discarded, even though adding an area child does not grow the root and containerAcceptsBlock(state, insertTarget) would accept it. Separate registry/allowlist checks from capacity and apply capacity only to the actual destination container.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcffefbbc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return;
}

if (!isBlockInstance(value) || seen.has(value.id)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quarantine nodes with noncanonical envelopes

When a standalone editable zone receives a recognizable block with an extra top-level property, isBlockInstance accepts it and this classifier treats it as valid. Editing that block preserves the extra property, unsafeZoneIds allows Save, and write schemas based on strict zodContentNode then reject the payload. Validate the complete storage envelope here so these legacy or externally supplied nodes enter the invalid-entry repair workflow.

Useful? React with 👍 / 👎.

onCompositionStart: handleCompositionStart,
onCompositionUpdate: handleCompositionStart,
onFocus: handleFocus,
onInput: handleInput,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compose the wrapped element's event callbacks

When the native element wrapped by BlockField already defines onInput, onKeyDown, onFocus, or another callback injected here, cloneElement replaces the block's handler with the editor handler. Any block behavior tied to those callbacks silently stops working in edit mode; compose each existing callback with the editor callback instead of overwriting it.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the editor/content-engine-visual-editor branch from e16d1c9 to 1cbeec1 Compare September 19, 2026 20:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cbeec11a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

className?: string;
id: string;
}) => (
<ContentZone className={className} id={id} registry={pageWidgetsRegistry()} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load the application registry for page widget zones

When a route uses the documented PageWidgetsZone abstraction without separately importing the generated blocks.gen.ts, pageWidgetsRegistry() falls back to a registry containing only core blocks. The core settings route does exactly this, and no application code imports the generated registry, so configured plugin widgets cannot be rendered or inserted there even though the API registry accepts them; previously stored plugin widgets also disappear from the page. Pass the application registry into this abstraction or ensure the generated registry is loaded by every route using it.

Useful? React with 👍 / 👎.

Comment on lines +64 to +68
if (!response.ok) return null;

const body: unknown = await response.json();

return isLayoutPayload(body) ? body : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Withhold zone defaults when the layout read fails

When this request fails or returns a malformed payload, the query resolves to null, but PageWidgets passes that to EditablePage, whose empty layout state resolves every declared zone to zone.default. A transient 500 or invalid response therefore renders the shipped defaults in place of the unknown stored layout, contrary to the stated null contract and potentially showing obsolete content; distinguish a failed read from the legitimate server response that contains effective defaults so failed reads render no zones.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db65e03a93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +234 to +237
const allowed = readAllowed(pageId, zoneId, declared.allowed);
const max = readCount(pageId, zoneId, "max", declared.max);
const min = readCount(pageId, zoneId, "min", declared.min);
const shipped = readDefault(pageId, zoneId, declared.default);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate shipped defaults against the zone allowlist

When a zone restricts allowed but its default contains another block type, including as an area child, defineEditablePage accepts the declaration because the subsequent checks cover only shape, IDs, and counts. The first layout GET then runs the default through zodBlockInstances, rejects the disallowed block, and returns 500 for every visitor; reject this mismatch while constructing the page definition.

Useful? React with 👍 / 👎.

Comment on lines +447 to +450
const pluginWidgets = readOptionalPluginModules<ResolvedBlocksModule>(
pluginIds,
WIDGETS_SUBPATH,
resolvePackageFile,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Watch plugins that do not yet export widgets

When a configured plugin has no resolvable widgets module at startup and one is created while vitnode dev is running, this discovery call returns neither a module nor a watch path. In the inspected configureServer logic, only the config, already-resolved files, and the host routes directory trigger regeneration, so the new compiled widgets.js never updates blocks.gen.ts until the app server is restarted or another watched input changes.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da7f309cec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +75 to +76
export const moderatorPermissionsQueryKey = () =>
["vitnode", "users", "permissions", "moderator"] as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear moderator permissions across identity changes

This identity-dependent response is cached under one global key, but removeUserIdentityQueries only removes file and device queries during sign-in, sign-out, and SSO transitions. If one user loads an editable page and another user subsequently signs in in the same browser, the loader can reuse the first user's cached permissions—especially because loadPageWidgets warms this query with staleTime: "static"—so the second user may incorrectly see or lose the Edit widgets action. Add this query family to the identity cleanup or scope its key by the current user.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
layout != null &&
permission.plugin !== undefined &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve omitted permission plugins before gating edits

When a plugin page uses the supported shorthand permission: { module, permission }, registerEditablePage resolves the missing plugin to the registering plugin on the server, but this browser path instead makes permission.plugin === undefined categorically non-editable. Consequently <PageWidgets> never offers editing—even to a root moderator—although the same user is authorized to save the page. Either carry the resolved plugin into the client definition or require/provide it explicitly at this abstraction boundary.

Useful? React with 👍 / 👎.

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 750c2cb3bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +49 to +55
configResolved: async () => {
const packageNames = [PACKAGE_NAME, ...(await readPluginIds(appRoot))];

directives = packageNames
.map(packageName => readBuildOutput(appRoot, packageName))
.filter(directory => directory !== null)
.map(sourceDirective)

ghost Sep 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh Tailwind sources after plugin-config changes

During vite dev, adding a plugin to vitnode.config.ts triggers vitNodePluginRoutes.configureServer to regenerate the plugin artifacts, but this plugin captures the configured package list only once in configResolved. The new plugin's components can therefore load while its build output is absent from Tailwind's @source directives, leaving its utility classes ungenerated until the developer restarts Vite. Recompute these directives when the configuration changes or force a server restart.

Useful? React with 👍 / 👎.

Comment on lines +23 to +30
return (
<LeaveConfirmDialog
onConfirm={() => blocker.proceed?.()}
onOpenChange={open => {
if (!open) blocker.reset?.();
}}
open={blocker.status === "blocked"}
/>

ghost Sep 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent leaving while a layout save is in flight

If a user clicks Save and navigates before the PUT resolves, the editor remains dirty and this blocker opens the leave dialog, but it does not pass saveStatus, so the destructive leave action is enabled as though no save were running. The client fetch has no abort signal, so choosing “leave without saving” can still let that request commit after navigation. Pass the saving state here to disable leaving, or cancel the in-flight request before proceeding.

Useful? React with 👍 / 👎.

Content zones and blocks
- Content node contract, block validation and shape checking, one
  definition of a stored node on every write path
- Content Zones runtime, zone capacity limits and bounds validation,
  block variants and the public area renderer
- `clearsToEmpty` on AutoForm fields, block field limits, refusal of
  content whose min can never be reached

Visual editor
- Visual edit mode with sidebar block insertion, properties panel,
  zone layout CRUD, drag and drop, inline field editing
- Layout areas and variant controls, block reference system, canonical
  snapshot handling on save, invalid entry repair
- Editable page widgets and layout management, edit-widgets entry in
  the user menu
- Area spacing controls: `clampAreaSpacing`, `AreaSpacingSlider`,
  margin controls in the properties panel, numeric layout gap, and an
  area frame that shows open widget slots

Supporting work
- Tailwind sources plugin for scanning styles
- Stage 4 editor i18n vocabulary, "blocks" renamed to "widgets" in the
  localization strings
- Example plugin zones page built with AutoForm, callout and features
  widgets
- Migrations: example block pages, example zones layouts, core page
  layouts
- Docs under dev/widgets and built-in events
- `apps/api` start script path corrected and `rootDir` set to `./src`
- Obsolete test files removed and vitest configuration updated

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aXenDeveloper
aXenDeveloper force-pushed the editor/content-engine-visual-editor branch from e698931 to cfae00b Compare September 20, 2026 12:37

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfae00b379

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +53 to +56
method: "get",
path: "/layout",
description:
"Read one registered page's effective layout: the zones it declares, each one the stored override or the shipped default. Public, because a visitor renders the page from it. An unknown page id is a 404.",

ghost Sep 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Protect layouts belonging to authenticated pages

The GET route returns every registered layout without checking the caller, but the newly registered core:settings page is rendered under defineAuthenticatedRoute in packages/vitnode/src/pages/settings/layout.tsx. An anonymous request to /pages/layout?pageId=core:settings can therefore read moderator-authored header and footer blocks intended for signed-in users. Add a per-page read-access policy and enforce it here rather than treating all editable pages as public.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit 7df7692 into canary Sep 20, 2026
@aXenDeveloper
aXenDeveloper deleted the editor/content-engine-visual-editor branch September 20, 2026 12:49

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2954edad93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +95
const full =
capacity !== null && !fitsZoneMax(capacity.max, capacity.blocks + 1);

ghost Sep 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the area's child cap in full

When an area already contains AREA_CHILDREN_DEFAULT_MAX children but the zone-wide maximum still has capacity, full remains false, so the Add block action stays enabled. Clicking it cannot insert anything because containerAcceptsBlock rejects the target via areaHasRoom, causing the target to be cleared and making the visible action a no-op. Treat the area as full when its child cap is reached.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant