Skip to content

feat(builder): a click inside a component selects the instance - #1724

Merged
mobeenabdullah merged 5 commits into
mainfrom
pb6-t6a-selection-reads-the-instance
Sep 11, 2026
Merged

mobeenabdullah merged 5 commits into
mainfrom
pb6-t6a-selection-reads-the-instance

Conversation

@mobeenabdullah

Copy link
Copy Markdown
Collaborator

T-6a slice 2, and the consumer #1694 shipped the marker for.

A component is inlined at render: the instance node is replaced by the tree its definition describes, and every element of that tree carries an id re-minted during composition — one the page's stored document does not contain.

So nodeIdFromEvent returned that id and onSelect was handed an address it cannot select, edit or delete. The drag path bailed instead, because findNode could not resolve it either — dragging inside a component already did nothing, by accident rather than by design.

The walk now asks the element it lands on whether the resolver marked it, and answers with the host instance when it did.

Asking the ELEMENT, not its ancestors

This is the design rather than a detail, and it is the part worth reviewing closely.

owner.closest("[data-nx-instance]") is the obvious implementation and it is wrong. An instance's slot content belongs to the page and is deliberately unmarked — but it renders nested inside the definition's marked box. A walk upwards finds that box and hands back the component for a node the author can and should select directly, which is exactly the content someone opened the editor to change.

The marker is per-node rather than a wrapper element precisely so the question can be answered without walking.

Break-verified against that exact form. Implementing closest() fails the slot-content case:

× answers with its OWN id for slot content the page supplied
× reads an EMPTY instance marker as page-owned

The inverse reader answers the same addresses

nodeElement and nodeIdFromEvent are one mapping read in two directions; separated, they become two answers to what a node id means in the DOM.

An instance id names a node that renders no element of its own, so chrome measuring the selection is pointed at the first element the definition contributed — the outermost, in document order. Without it, selecting inside a component would draw its outline nowhere. Break-verified: removing the instance lookup fails that case.

A node's own element still wins where both exist. They cannot collide today — an instance is replaced rather than rendered — and stating the precedence keeps it from being iteration order.

An empty marker reads as page-owned

The renderer never emits one; it treats an empty id as no provenance. But an address of "" is not selectable, and reading it as page-owned leaves the element addressable by its own id rather than by nothing.

Verified

  • packages/builder: 3079 tests pass, lint 0, check-types 0
  • fallow audit --base origin/main: pass, no introduced complexity or dead code
  • changeset status: valid
  • Break-verified in both directions, against the wrong implementations rather than by mutation

A component is inlined at render, so every element of the definition's tree
carries an id re-minted during composition — one the page's stored document does
not contain. `nodeIdFromEvent` returned that id and `onSelect` was handed an
address it cannot select, edit or delete. The drag path bailed instead, because
`findNode` could not resolve it either, so dragging inside a component already
did nothing by accident.

The walk now asks the element it lands on whether the resolver marked it, and
answers with the host instance when it did.

ASKING THE ELEMENT rather than its ancestors is the design, not a detail.
`owner.closest("[data-nx-instance]")` is the obvious implementation and it is
wrong: an instance's slot content belongs to the page and is deliberately
unmarked, but it renders NESTED INSIDE the definition's marked box, so a walk
upwards finds that box and hands back the component for a node the author can
and should select directly — which is exactly the content a marketer opened the
editor to change. The marker is per-node rather than a wrapper element precisely
so the question can be answered without walking, and `getAttribute` on the
element is what answers it. Break-verified against the `closest()` form, which
fails the slot-content case.

An empty marker reads as page-owned. The renderer never emits one — it treats an
empty id as no provenance — but an address of "" is not selectable, and reading
it as page-owned leaves the element addressable by its own id rather than by
nothing.

`nodeElement` answers the same addresses, because the two are one mapping read
in two directions. An instance id names a node that renders no element of its
own, so a caller measuring the selection is pointed at the first element the
definition contributed — the outermost, in document order. A node's own element
still wins where both exist; they cannot collide today, and stating the
precedence keeps it from being iteration order.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-11T07:49:58.353019Z c19dd26 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 41 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6a66ec59-5110-4185-b7e9-ccc621dc7914

📥 Commits

Reviewing files that changed from the base of the PR and between 705926b and c19dd26.

⛔ Files ignored due to path filters (1)
  • .changeset/selection-reads-the-instance.md is excluded by !.changeset/**
📒 Files selected for processing (4)
  • packages/builder/src/canvas-drag.tsx
  • packages/builder/src/canvas.test.tsx
  • packages/builder/src/canvas.tsx
  • packages/builder/src/style-subject.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: d0e89d68dd

ℹ️ 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/builder/src/canvas.tsx Outdated
// canvas rendered within another rendered page would otherwise resolve a
// click to the outer page's node.
if (owner.closest(`.${CANVAS_ROOT_CLASS}`) === null) return null;
return addressOf(owner);

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 Translate component addresses in the context-menu path

When a definition-owned element is right-clicked or touch-long-pressed, this translation is bypassed: both the canvas context handler and BlockContextMenu.noteTarget call contextMenuTargetOf, which still returns the re-minted NODE_ID_ATTRIBUTE value from canvas.tsx:164-184. Since that ID is absent from the stored document, applySelection retains the previous selection and the menu can execute destructive actions such as Delete against that previous block rather than the component under the pointer. Derive the context-menu target from the same instance-aware address used here.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed, reproduced and fixed at eacbc7134. This is the sharpest of the three because the consequence is destructive.

You are right on the path: contextMenuTargetOf returned the raw NODE_ID_ATTRIBUTE, applySelection ignores an id the stored document does not contain, so the PREVIOUS selection stood — and the menu's verbs, Delete among them, acted on a block nobody was pointing at.

The finding is really about a partial migration, and the fix is the general one rather than this call site. I translated the click path and the geometry lookup and left three other readers of the same element-to-node mapping on the raw attribute: this one, the selection markers, and the drag rectangles. Each is its own defect and they have one cause.

So nodeAddressOf is exported and all three ask it. Patching contextMenuTargetOf alone would have left the same shape waiting in the other two.

One rule fell out of doing all three at once, and it is the part worth reviewing. An address sits on several elements for two reasons that need OPPOSITE answers:

  • a node a block renders more than once puts one id on SIBLING elements, and every copy is a rendering of that node — an existing test pins that marking one outlines a single row of ten;
  • a component instance puts one address on every element its definition contributed, NESTED, and those are renderings of different definition nodes that merely share a host.

My first attempt used "first in document order" and broke the first case; the suite caught it. isOutermostForAddress asks about ENCLOSURE instead, which separates them exactly. The host marker is contiguous over a subtree, so the nearest enclosing one carrying the same value is the whole test.

Break-verified: restoring the raw attribute here fails the new context-menu case while the page-owned control stays green.

Comment on lines +315 to +316
if (hosted === null && element.getAttribute(INSTANCE_ATTRIBUTE) === id) {
hosted = element;

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 Mark selected instances using their stored address

When the new hit test selects an instance ID, useCanvasMarkers still compares the selection only with each element's re-minted NODE_ID_ATTRIBUTE at canvas.tsx:879-903. Consequently no definition-owned element receives data-nx-selected, so the component has no selection outline and forced interaction states—which are derived from the primary selection marker—are not shown. The marker walk needs to derive its target from the same instance-aware address rather than maintaining the old node-ID-only mapping.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed at eacbc7134, as part of putting every reader of the element-to-node mapping on one rule — this was one of three left on the raw attribute.

The marker walk compared each element's NODE_ID_ATTRIBUTE against the selection, so a definition-owned element never matched an instance id: no data-nx-selected, therefore no outline, and no forced interaction state, since those derive from the same marker.

What I got wrong on the first attempt is worth reporting. Marking every element that carries the address seemed obviously right, so I deduplicated to one per address by taking the first in document order. That broke an existing test: a block that renders its child twice puts one id on two SIBLING elements, and BOTH must be marked — marking one outlines a single row of ten while previewing the state on another.

So "one element per address" and "every rendering of the node" are both real requirements, and they are not in conflict once the right question is asked. It is ENCLOSURE:

  • sibling copies of one node — neither encloses the other, both outermost, both marked;
  • an instance's inlined elements — nested under the definition's box, so only the box is marked.

isOutermostForAddress asks exactly that, and the host marker being contiguous over a subtree means the nearest enclosing one carrying the same value is the whole test — no walk to the root.

The same helper keys the drag rectangles, so the outline and the geometry describe one box rather than two elements chosen by different rules.

// that as no provenance — but an address of "" is not selectable, so reading
// it as page-owned is the answer that leaves the element addressable by its
// own id.
if (instance !== null && instance !== "") return instance;

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 Index drag rectangles by instance address

When a resolved component is dragged, useCanvasDrag now receives the host instance ID from this helper and gets past its previous findNode bailout, but snapshotRects in canvas-drag.tsx:387-408 still keys every rectangle only by the re-minted data-nx-node value. collectRegions subsequently requests rectangles by stored instance ID, so on a page made of component instances every child is treated as unrendered and targetsInRegion collapses the root to a single index-0 midpoint; releasing an activated drag can therefore move the component to the start regardless of where it was dropped. Derive the rectangle keys from the same instance-aware address before enabling this drag path.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed at eacbc7134. This is the one I most needed pointed out, because this branch is what made it reachable — it is a regression I introduced rather than a defect I merely failed to fix.

Your chain is exact. snapshotRects keyed every rectangle by the raw data-nx-node, collectRegions asks by the address, so on a page of component instances every child measured as unrendered; targetsInRegion then collapsed the root to a single index-0 midpoint and releasing an activated drag moved the component to the start whatever was under the pointer.

It could not happen before this branch. The drag bailed earlier, because findNode could not resolve the re-minted id — the PR description even noted that dragging inside a component "already did nothing, by accident rather than by design". Getting the hit test past that bailout is precisely what exposed the rect indexing, so the fix that enabled the path owed this one and did not pay it.

Keys come from nodeAddressOf now, with the same outermost rule the selection markers use, so the drag geometry and the outline describe one box.

The outermost rule is doing real work here rather than being tidy. The previous keying was last-wins by iteration, so an instance would have measured whichever element its definition contributed LAST — an inner leaf. Taking the outermost is what makes the rectangle the component's own. Sibling copies of a node a block rendered twice are each outermost, so that case keeps the last-wins behaviour it had.

I have not added a drag-level regression test for the collapse itself, and I would rather say so than imply the coverage is wider than it is: the new tests pin the addressing and the enclosure rule, which is the cause, but not targetsInRegion's behaviour end to end. If you would rather see that pinned before this lands, say so and I will build the fixture.

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Whole-Repository Code Hygiene Summary

Full dead-code, duplication, and complexity report for the PR branch as it stands now. Playground is excluded. Quality gate enforcement on introduced issues is performed by the Changed files job.

🌿 Fallow

Warning

Review needed

⚠️ 73 code issues · ⚠️ 694 clone groups · ⚠️ 1045 health findings

See inline review comments for per-finding details.

Code issues (73)
Category Count
Unused files 2
Unused exports 5
Unused dependencies 19
Unused devDependencies 6
Unresolved imports 2
Unlisted dependencies 1
Circular dependencies 38
Duplication (694 groups · 29077 lines · 4.1%)
Locations Lines Tokens
schemas/_dialect-bundles/mysql.relations.ts:40-134
schemas/_dialect-bundles/postgres.relations.ts:40-134
schemas/_dialect-bundles/sqlite.relations.ts:40-134
95 593
cli/commands/db-sync-demote.ts:70-75
cli/commands/db-sync-promote.ts:38-43
cli/commands/dev-build.ts:100-105
cli/commands/dev-build.ts:179-184
cli/commands/dev-build.ts:299-304
cli/commands/dev-build.ts:411-416
cli/commands/dev-build.ts:552-557
cli/commands/dev-server.ts:575-580
cli/commands/dev-server.ts:840-845
cli/commands/dev-server.ts:1143-1148
cli/commands/migrate-field-groups.ts:110-115
6 70
entries/EntryList/EntryTableSkeleton.tsx:74-98
collection/components/CollectionTableSkeleton.tsx:94-118
field-group/components/FieldGroupTableSkeleton.tsx:90-114
plugins/components/PluginsTableSkeleton.tsx:86-110
singles/components/SinglesTableSkeleton.tsx:77-101
src/components/table-skeleton.tsx:100-124
25 89
collections/config/validate-config.ts:380-433
field-groups/config/validate-field-group.ts:185-238
singles/config/validate-single.ts:190-243
54 152
dispatcher/handlers/collection-dispatcher.ts:922-964
field-groups/services/field-group-table-provisioning.ts:186-236
singles/services/reconcile-single-companion.ts:110-160
51 149

… and 689 more groups.

Across 423 files.

Complexity (1045 functions above threshold)
File Function Severity Cyclomatic Cognitive CRAP Lines
singles/services/single-mutation-service.ts:981 <arrow> critical 251 ! 324 ! 13859.2 ! 1625
collections/services/collection-mutation-service.ts:6209 <arrow> critical 174 ! 177 ! 6713.6 ! 1301
src/init/reload-config.ts:1319 applyReload critical 144 ! 228 ! 4623 ! 1433
shared/lib/entry-validation.ts:223 validateFieldValue critical 109 ! 157 ! 2675.3 ! 432
blocks-engine/src/measure-bytes.ts:646 surveyDocument critical 102 ! 250 ! 137.1 ! 658

4992 files, 76336 functions analyzed (thresholds: cyclomatic > 20, cognitive > 15, CRAP >= 30)

Codebase health

Metric Value
Maintainability 91.7 / 100
Avg complexity 1.8

Tip

Run fallow fix --dry-run to preview auto-fixes.
Add /** @public */ above exports to preserve them.

@github-actions github-actions Bot added the type: docs Documentation only label Sep 10, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@nextlyhq/adapter-drizzle

npm i https://pkg.pr.new/@nextlyhq/adapter-drizzle@50a765e

@nextlyhq/adapter-mysql

npm i https://pkg.pr.new/@nextlyhq/adapter-mysql@50a765e

@nextlyhq/adapter-postgres

npm i https://pkg.pr.new/@nextlyhq/adapter-postgres@50a765e

@nextlyhq/adapter-sqlite

npm i https://pkg.pr.new/@nextlyhq/adapter-sqlite@50a765e

@nextlyhq/admin

npm i https://pkg.pr.new/@nextlyhq/admin@50a765e

@nextlyhq/admin-css

npm i https://pkg.pr.new/@nextlyhq/admin-css@50a765e

@nextlyhq/blocks-engine

npm i https://pkg.pr.new/@nextlyhq/blocks-engine@50a765e

@nextlyhq/blocks-react

npm i https://pkg.pr.new/@nextlyhq/blocks-react@50a765e

@nextlyhq/builder

npm i https://pkg.pr.new/@nextlyhq/builder@50a765e

create-nextly-app

npm i https://pkg.pr.new/create-nextly-app@50a765e

@nextlyhq/eslint-plugin

npm i https://pkg.pr.new/@nextlyhq/eslint-plugin@50a765e

nextly

npm i https://pkg.pr.new/nextly@50a765e

@nextlyhq/plugin-form-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-form-builder@50a765e

@nextlyhq/plugin-page-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-page-builder@50a765e

@nextlyhq/plugin-sdk

npm i https://pkg.pr.new/@nextlyhq/plugin-sdk@50a765e

@nextlyhq/plugin-seo

npm i https://pkg.pr.new/@nextlyhq/plugin-seo@50a765e

@nextlyhq/storage-s3

npm i https://pkg.pr.new/@nextlyhq/storage-s3@50a765e

@nextlyhq/storage-uploadthing

npm i https://pkg.pr.new/@nextlyhq/storage-uploadthing@50a765e

@nextlyhq/storage-vercel-blob

npm i https://pkg.pr.new/@nextlyhq/storage-vercel-blob@50a765e

@nextlyhq/ui

npm i https://pkg.pr.new/@nextlyhq/ui@50a765e

commit: 50a765e

The first version translated the click path and the geometry lookup and left
three other readers on the raw attribute. Each is a way a definition-owned
element addresses nothing.

The CONTEXT MENU returned the re-minted id, which `applySelection` ignores
because the stored document does not contain it — so a right-click inside a
component left the PREVIOUS selection in place and the menu's verbs acted on a
block nobody pointed at. Delete among them.

The SELECTION MARKERS compared each element against the raw attribute, so a
selected component received no marker at all: no outline, and no forced
interaction state, both of which derive from it.

The DRAG RECTANGLES were keyed by the raw attribute while `collectRegions` asks
by the address, so every child of a component measured as unrendered and
`targetsInRegion` collapsed the root to one index-0 midpoint — releasing a drag
moved the component to the start whatever was under the pointer. Reachable only
because this branch got the drag past its old `findNode` bailout, so making that
path work is what exposed it.

`nodeAddressOf` is exported for them, and one more rule came out of it.

An address can sit on several elements for two reasons that need OPPOSITE
answers. A node a block renders more than once puts one id on SIBLING elements,
and every copy is a rendering of that node — marking one outlines a single row
of ten, which an existing test pins and which the first attempt here broke. A
component instance puts one address on every element its definition
contributed, NESTED, and those are renderings of different definition nodes that
merely share a host — marking all of them outlines everything inside the
component, and measuring all of them keys the geometry to whichever was visited
last.

Enclosure separates them, so `isOutermostForAddress` asks exactly that. The host
marker is contiguous over a subtree, so the nearest enclosing one carrying the
same value is the whole test.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex 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: eacbc71340

ℹ️ 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/builder/src/canvas-drag.tsx Outdated
Comment on lines +412 to +413
if (id !== null && isOutermostForAddress(element, id)) {
measured.set(id, canvasContentRect(element, root));

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 Aggregate every root in a component's drag rectangle

When a component definition has multiple top-level nodes, every root is a sibling and therefore passes isOutermostForAddress; this assignment then overwrites the same instance ID for each root and retains only the last root's rectangle. Multi-root definitions are explicitly supported by resolveComponentInstances, so collectRegions will order drops against only that final element rather than the component's full rendered extent, allowing a drop near an earlier root to resolve to the wrong before/after index. Aggregate the outermost rectangles belonging to an instance instead of using last-write-wins.

Useful? React with 👍 / 👎.

Comment on lines +919 to +922
// The ADDRESS, not the raw attribute. A definition-owned element
// carries a re-minted id the selection never holds, so comparing the
// attribute leaves a selected component with no outline at all.
const id = nodeAddressOf(element);

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 Reuse instance addresses for drag and lock markers

When a definition-owned component is locked or becomes the active drag source, this translated instance address flows into lockedIds and draggingId, but the immediately following drag-marker walk at lines 972-982 recomputes the raw NODE_ID_ATTRIBUTE. Because that re-minted ID never equals the stored instance ID, locked components still advertise the grab cursor and dragged components never receive the dimming marker. Use nodeAddressOf (with the same outermost policy) for those markers as well.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

Comment on lines +358 to +361
// The node's OWN element wins where both exist. They cannot today — an
// instance is replaced rather than rendered — but the precedence states which
// answer is the node's rather than leaving it to iteration order.
return own ?? hosted;

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 Scope component element lookup to its canvas

When an outer canvas contains a block that renders another canvas, IDs are unique only within each document, as ownedNodeElement below already notes. If the inner document has an ordinary node whose ID equals the selected outer component instance ID, this unscoped scan populates own with that foreign element and own ?? hosted chooses it over the correct instance rendering regardless of document order. BlockToolbar and SpacingOverlay consequently measure and position themselves against the inner canvas; restrict both matches with the existing canvas-ownership predicate.

Useful? React with 👍 / 👎.

Comment on lines +259 to +263
* EXPORTED because it is the one rule, and every reader of the element-to-node
* mapping has to use it. Slice one of this work updated the click path and the
* geometry lookup and left three other readers on the raw attribute — the
* context menu, the selection markers and the drag rectangles — which is how a
* right-click came to act on the previously selected block.

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 Remove task-history commentary from the helper

Rewrite this documentation to state only the current element-to-node invariant: describing what “Slice one of this work” updated and which readers it previously left behind records task and review history in a production-code comment, contrary to the repository's explicit requirement that comments describe code only and never reference tasks, plans, conversations, or review findings. The similar historical explanation in snapshotRects should be removed at the same time.

AGENTS.md reference: AGENTS.md:L277-L279

Useful? React with 👍 / 👎.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex 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: 50a765e214

ℹ️ 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".

// The ADDRESS, not the raw attribute. A definition-owned element
// carries a re-minted id the selection never holds, so comparing the
// attribute leaves a selected component with no outline at all.
const id = nodeAddressOf(element);

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 Skip foreign canvases when marking instance selections

When an outer canvas selects a component whose instance ID is also used by a component in a nested canvas, touched includes the nested canvas elements and this translated address makes the inner component appear selected. Unlike the drag-marker pass below, this pass never calls ownedByCanvas, so the outer effect can add a primary outline and forced interaction state to a block belonging to another document. Filter by canvas ownership before comparing addresses.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

Comment on lines +320 to +322
const host = element.parentElement?.closest(`[${INSTANCE_ATTRIBUTE}]`);
if (host === null || host === undefined) return true;
return host.getAttribute(INSTANCE_ATTRIBUTE) !== address;

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 Bound the outermost lookup to the owning canvas

When a canvas is rendered inside a definition-owned element and its document reuses the enclosing component's instance ID—which is valid because IDs are document-scoped—this unbounded closest escapes the inner canvas and finds the outer document's marker. The inner component is then classified as non-outermost, so it receives no selection marker and snapshotRects omits its rectangle, degrading drag placement. Stop the ancestor search at the component's owning canvas.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

// canvas rendered within another rendered page would otherwise resolve a
// click to the outer page's node.
if (owner.closest(`.${CANVAS_ROOT_CLASS}`) === null) return null;
return nodeAddressOf(owner);

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 canvas readings by instance address

When this hit test selects a definition-owned element, it now returns the stored instance ID, but useCanvasReading still reaches markedElementOf, which compares that ID only with raw NODE_ID_ATTRIBUTE values. Because an inlined instance has no element carrying its own ID there, the style inspector permanently reads both its rendered tag and orientation as unknown, losing tag-dependent provenance and the correctly oriented logical-side editor. Route that reverse lookup through the same instance-aware element mapping.

AGENTS.md reference: AGENTS.md:L288-L291

Useful? React with 👍 / 👎.

… the one rule

Three ways a nested canvas — the shape component edit mode produces, a canvas
rendered inside a definition-owned element — could be read wrongly by the
canvas around it. Ids are scoped per document, so the inner document may hold
an instance with the same address as one selected outside, and each defect is
that address crossing the boundary.

The selection pass marked every element the outer container could query,
including the inner canvas's, and unlike the drag pass it never asked which
canvas an element belonged to. It could outline a block in a document the
selection is not in, and on a later pass clear a mark the inner canvas had
set. It now leaves foreign canvases alone, as the drag pass already did.

The outermost lookup searched ancestors without bound, so from inside the inner
canvas it found the OUTER document's instance marker, classified the inner
component as nested inside itself, and left it with no outline and no drag
rectangle. The search now stops at the canvas that owns the element.

And the style inspector's reverse lookup — which element a selected node is
drawn as — read the node-id attribute directly, so for an instance, which
renders no element carrying its own id, it found nothing: tag and orientation
read as unknown for as long as a component was selected. That lookup was a
sixth reader of the element-to-node mapping beside the five that already go
through `nodeElement`; it is now an adapter over that one rule rather than a
second search.

Each case break-verified by restoring the wrong implementation.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

All three reproduced and fixed in c19dd26, each break-verified by restoring the wrong implementation.

Selection pass over foreign canvases — confirmed, and it was worse than marking: on a later pass it would also clear a mark the inner canvas had set. It now skips elements another canvas owns, exactly as the drag pass already did. The test nests a canvas root inside a block and selects an address only the inner document holds.

Unbounded outermost lookup — confirmed. The ancestor search now stops at the canvas that owns the element; a host found beyond it belongs to another document and cannot make this component nested.

markedElementOf reading the raw attribute — confirmed, and this one was a sixth reader of the element-to-node mapping beside the five that already go through nodeElement. It is now an adapter over that one rule rather than a second search, so the style inspector reads an instance's tag and orientation.

The branch was also merged onto main past #1761, so its integration legs run against the repaired golden test.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex 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: c19dd26802

ℹ️ 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".

// canvas rendered within another rendered page would otherwise resolve a
// click to the outer page's node.
if (owner.closest(`.${CANVAS_ROOT_CLASS}`) === null) return null;
return nodeAddressOf(owner);

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 Scope click addresses to the current canvas

When one canvas is rendered inside another, this check only proves that the owner belongs to some canvas. A click on an inner component bubbles to the outer canvas, and because IDs are document-scoped, the inner instance may validly share an ID with an outer node; this new translation then returns that stored instance ID to both handlers, so the outer onSelect can select its unrelated node and subsequent keyboard or toolbar actions target the wrong document. Pass the current canvas root into this lookup and reject owners whose nearest canvas differs, as contextMenuTargetOf already does.

AGENTS.md reference: AGENTS.md:L294-L297

Useful? React with 👍 / 👎.

@mobeenabdullah
mobeenabdullah merged commit c0c8c52 into main Sep 11, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant