Skip to content

feat(mind): navigate selected node with arrow keys#1147

Open
nightt5879 wants to merge 6 commits into
developfrom
nightt5879/mind-arrow-key-navigation
Open

feat(mind): navigate selected node with arrow keys#1147
nightt5879 wants to merge 6 commits into
developfrom
nightt5879/mind-arrow-key-navigation

Conversation

@nightt5879

@nightt5879 nightt5879 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add arrow-key navigation for a single selected child mind node while preserving root-mind movement and text-editing behavior.
  • Navigate by visible mind-map structure first: parent, children, previous sibling, and next sibling are collected as layout-aware relationship candidates.
  • Resolve each relationship from the layout context that owns it, including nested layouts.
  • Resolve direction conflicts explicitly with this priority: sibling → parent → remembered child → first child.
  • Keep geometry lookup isolated as a fallback only when no layout candidate matches.

Why

A selected node can participate in relationships owned by different layout contexts. In a nested rightTopIndented -> right tree, the outer layout owns the node's incoming edge and sibling sequence, while the nested right layout owns its descendants.

Nested subtrees may also be mirrored by an ancestor. Sibling navigation therefore uses the common parent's final rendered up / left orientation instead of relying only on the declared layout enum. This prevents loops such as A-1 -> A-2 -> A-1 when navigating a mirrored three-sibling subtree.

Scope

This PR covers layout-based keyboard navigation and keeps the existing geometry lookup as an isolated fallback. Broader geometry policy changes and shared rendered-direction refactors for Node More, DnD, link drawing, and other callers remain follow-up work.

Validation

  • npx ng test mind --watch=false --progress=false --browsers=ChromeHeadless --include=packages/mind/src/plugins/with-mind-hotkey.spec.ts — 24 specs passed
  • npx ng test mind --watch=false --progress=false --browsers=ChromeHeadless — 45 specs passed
  • npm run build:mind
  • npm run build:demo
  • Targeted ESLint, Prettier, and git diff --check for the changed files
  • Manual review route: http://localhost:4200/?init=mind-navigation-review
    • A-1 ↑ A-2 ↑ 111
    • 111 ↓ A-2 ↓ A-1
    • standard boundary: D ↓ E ↑ D

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 26, 2026

Copy link
Copy Markdown

Deploying plait with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3abcb42
Status: ✅  Deploy successful!
Preview URL: https://d0a5f94c.plait.pages.dev
Branch Preview URL: https://nightt5879-mind-arrow-key-na.plait.pages.dev

View logs

@nightt5879 nightt5879 marked this pull request as ready for review June 26, 2026 09:54
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 26, 2026

Copy link
Copy Markdown

Deploying plait-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3abcb42
Status: ✅  Deploy successful!
Preview URL: https://b3825d69.plait-docs.pages.dev
Branch Preview URL: https://nightt5879-mind-arrow-key-na.plait-docs.pages.dev

View logs

@nightt5879 nightt5879 requested a review from pubuzhixing8 June 26, 2026 09:59
@pubuzhixing8

Copy link
Copy Markdown
Collaborator

Thanks for the update. The overall idea of using geometry to navigate between visible mind nodes looks good to me: collect visible nodes, filter by direction, then sort by cross-axis overlap and distance. That makes the behavior more general than relying on tree structure only.

A few suggestions:

  1. The arrow-key navigation is handled before the core arrow-moving plugin. Since the root mind element is movable, this may change the existing behavior where arrow keys can move the selected root mind. If the expected behavior is to navigate only between child mind nodes, we may want to exclude the root node here, e.g. !PlaitMind.isMind(targetElement). If navigating from the root is intentional, it would be good to confirm this product behavior explicitly.

  2. selectMindElement currently calls cacheSelectedElements and then Transforms.setSelection. Since with-selection recalculates selected elements after a set_selection operation based on hit testing, the manual cache update is mostly temporary and the final result depends on the center point hit result. I think this could be simplified by only setting the selection point, or the function name could be adjusted to make that dependency clearer.

  3. The extracted navigation helpers are clear, but they feel more like reusable position/navigation utilities than hotkey-specific code. Consider moving getNextMindElementByDirection, hasCrossAxisOverlap, and the distance comparison helpers into utils/position or a dedicated utils/navigation module. That would make the hotkey plugin thinner and allow the same node-location logic to be reused later.

  4. For direction modeling, we could reuse the existing core Direction enum (left, right, top, bottom) and helpers like isHorizontalDirection, instead of introducing a separate NavigationDirection with up/down. This would align the implementation with the rest of the codebase.

  5. hasCrossAxisOverlap can probably be expressed with existing rectangle helpers, such as RectangleClient.isHitX / isHitY, which would reduce custom coordinate logic.

  6. It may be worth adding a few more tests for edge cases:

    • selected root mind and arrow-key behavior
    • collapsed nodes should not navigate into hidden descendants
    • no candidate in the navigation direction should not prevent the default behavior
    • left/right split layout and indented layout cases

@nightt5879

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed suggestions. I pushed 11f04a69 and addressed the six points as follows:

  1. Root mind arrow-key behavior: I kept the existing behavior for the selected root mind by excluding root mind from the navigation path. Arrow-key navigation now only handles child mind nodes; when the root mind is selected, the event falls through to the core arrow-moving logic.

  2. selectMindElement: simplified it to only set the collapsed selection point at the target node center. The final selected element is now left to with-selection to recalculate from hit testing, so we no longer manually call cacheSelectedElements there.

  3. Navigation helpers: moved the reusable geometry/navigation logic out of the hotkey plugin into utils/position/navigation, keeping with-mind-hotkey focused on keyboard handling.

  4. Direction modeling: replaced the local NavigationDirection type with the core Direction enum (left, right, top, bottom) and reused isHorizontalDirection.

  5. Cross-axis overlap: replaced the custom overlap checks with RectangleClient.isHitX / RectangleClient.isHitY.

  6. Tests: expanded the coverage for the edge cases you mentioned: selected root mind behavior, collapsed nodes not navigating into hidden descendants, no candidate not preventing default behavior, and an indented-layout navigation case. The main arrow navigation test now also goes through the real selection recalculation flow.

Validated with:

  • npx ng test mind --watch=false --browsers=ChromeHeadlessCI --include=packages/mind/src/plugins/with-mind-hotkey.spec.ts --progress=false
  • npx ng test mind --watch=false --browsers=ChromeHeadlessCI --progress=false
  • npm run build:mind

@pubuzhixing8

Copy link
Copy Markdown
Collaborator

One more thought about the navigation model: the current geometry-based lookup is a good generic fallback, but for mind maps it may not always match the user's mental model. In many cases arrow-key navigation is expected to follow the mind-map structure first: left/right should usually move between parent and child depending on the branch direction, while up/down should usually move between visible siblings. The geometry-based nearest-node lookup can then be used as a fallback when there is no clear structural target.

This would make the behavior more semantic for standard mind-map navigation and avoid jumping to a visually nearby node from a different branch when a parent/child/sibling target is structurally more appropriate.

Could we consider making the current geometry lookup a fallback, and add a structure-first lookup for the common parent/child/sibling navigation cases?
Screenshot 2026-06-29 at 21 09 48

@nightt5879

nightt5879 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for pointing this out. We did some research and agree with your direction: mind-map arrow navigation should be structure-first instead of purely geometry-first.

I pushed an update that changes the main navigation path to use the mind node relationships first:

  • Left / right navigation now prefers parent-child movement based on the branch direction.
  • Up / down navigation moves between visible siblings.
  • Geometry-based navigation is kept only as a fallback.

We also tightened the fallback so it only considers candidates with cross-axis overlap. This keeps the fallback useful for layout edge cases, but avoids jumping across unrelated branches or to distant nodes when there is no valid structural target.

test use this:
image

@pubuzhixing8

Copy link
Copy Markdown
Collaborator

I tested the standard layout locally and the current behavior looks correct to me. Moving from the last right-side child to the first left-side child with ArrowDown is expected, so I don't think we need to split standard children by rightNodeCount for this interaction.

The remaining issue is more about how the structure navigation derives its direction model. Currently getNextMindElementByStructure hardcodes:

  • left/right -> parent/child
  • top/bottom -> sibling

This works for horizontal layouts, but it does not map cleanly to vertical layouts like upward / downward, where the parent-child axis is vertical and the sibling axis is horizontal.

I think we can make this more generic by reusing the layout-direction concepts already present in the codebase instead of adding more layout-specific branches. For example:

  • MindQueries.getCorrectLayoutByElement already resolves the effective branch layout, including standard left/right branches.
  • point-placement.ts#getLayoutDirection converts a rendered MindNode into a LayoutDirection using left / up.
  • node-more.generator.ts#getNodeMoreLayoutDirection is a useful reference for deriving a layout direction from an element.
  • The DnD detector also uses layout direction/correction concepts to avoid hardcoding behavior only by key direction.

A possible model would be:

  1. Derive the current branch's parent-child direction from the rendered node / correct layout.
  2. Use that direction for child navigation.
  3. Use the reverse direction for parent navigation.
  4. Treat the cross-axis directions as sibling navigation.
  5. Keep geometry lookup as the fallback when no structural target exists.

That would let horizontal, vertical, standard, and indented layouts share the same conceptual navigation model, instead of encoding left/right = parent/child and top/bottom = siblings directly.

@pubuzhixing8

Copy link
Copy Markdown
Collaborator

Concretely, getParentOrChildByHorizontalDirection could probably be renamed and generalized to something like getParentOrChildByLayoutDirection, where the parent-child axis is derived from the element's effective LayoutDirection rather than assuming it is always horizontal.

Screenshot 2026-06-30 at 22 41 57

Maybe we could handle the first two layouts, and handle AbstractNode later.

@nightt5879

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed direction. I reworked the arrow-key navigation to be layout-direction based instead of hard-coding horizontal parent/child and vertical sibling behavior.

What changed in the production fix:

  • derive parent/child navigation direction from the rendered layout;
  • keep standard split sibling navigation within the same side, so left-side and right-side first-level nodes do not jump across each other;
  • handle indented layouts by separating the root branch direction from deeper hierarchy direction;
  • restrict geometry fallback so it does not jump across indented hierarchy boundaries.

I added regression coverage for:

  • standard split same-side sibling navigation;
  • upward layout parent/child/sibling navigation;
  • right-bottom and right-top indented layouts;
  • indented boundary cases where geometry fallback previously jumped to cousin/uncle nodes.

Verified locally:

  • npx ng test mind --watch=false --browsers=ChromeHeadlessCI --include=packages/mind/src/plugins/with-mind-hotkey.spec.ts --progress=false -> 19 specs passing
  • npx ng test mind --watch=false --browsers=ChromeHeadlessCI --progress=false -> 40 specs passing
  • npm run build:mind
  • npm run build:demo

I also added a separate demo commit for manual review. After checking out this branch, run npm start and open:

http://localhost:4200/?init=mind-navigation-review

That demo commit is optional. If you prefer not to keep manual review data in the repo, we can drop chore(demo): add mind navigation review scenarios before merge.

@pubuzhixing8 pubuzhixing8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The structure-first approach is a good improvement, but I think the current navigation model still needs adjustment before merging, especially because nested layouts are already supported.

Problem: direction ownership in nested layouts

The effective layout of source does not necessarily describe how source is positioned relative to its parent or siblings.

For example, consider a right layout nested inside a rightTopIndented layout. The nested node's right layout should control how its own descendants expand. However, its incoming parent-child relationship and its position in the sibling sequence are still controlled by the outer layout context. In this case, the next sibling is reached with ArrowUp, but the current implementation derives sibling directions from source and incorrectly treats it as a normal right-layout node.

The direction should be resolved from the owner of each relationship:

  • parent -> child uses the layout context that arranges that edge;
  • previous/next sibling directions use the common parent's layout context;
  • child.layout controls child -> descendants, but does not redefine the incoming edge or sibling axis.

This also means that a single selected node may legitimately use different layout contexts for its incoming, outgoing, and sibling relationships.

Build layout-aware candidates before selecting a target

The current lookup obscures this relationship:

return (
    getVisibleSiblingByDirection(board, source, direction) ||
    getParentOrChildByLayoutDirection(board, source, direction, previousElement)
);

This reads as "try sibling navigation first, then fall back to parent/child navigation". Parent, child, previous sibling, and next sibling are layout-aware navigation candidates rather than fallback strategies. I suggest collecting and resolving all candidates first:

type LayoutNavigationRelation =
    | 'parent'
    | 'child'
    | 'previous-sibling'
    | 'next-sibling';

interface LayoutNavigationCandidate {
    element: MindElement;
    relation: LayoutNavigationRelation;
    direction: LayoutDirection;
}

The flat candidate list avoids duplicating the relation in both a model property and the candidate itself, while preserving enough semantic information for tests and debugging.

The resulting navigation flow is:

  1. Convert the keyboard direction to LayoutDirection.
  2. Collect the visible parent, children, previous sibling, and next sibling.
  3. Resolve each relationship from the layout context that owns it.
  4. Build LayoutNavigationCandidate[].
  5. Filter candidates by the requested direction.
  6. Resolve relation conflicts explicitly: sibling first, then parent, then a remembered child, then the first child.
  7. Use geometry lookup only when no layout candidate matches.

Resolve relationship directions explicitly

I suggest introducing a focused layout-direction.ts for the new navigation relation logic:

type LayoutRelation =
    | {
          type: 'parent-child';
          parent: MindElement;
          child: MindElement;
      }
    | {
          type: 'sibling';
          parent: MindElement;
          order: 'previous' | 'next';
      };

export const resolveLayoutRelationDirection = (
    board: PlaitBoard,
    relation: LayoutRelation
): LayoutDirection => {
    // The implementation resolves direction from relation.parent's
    // layout context, rather than from the selected source node.
};

The following is an implementation sketch showing the intended ownership rules. The exact helper names can follow the existing conventions:

const isLayoutRoot = (element: MindElement) => {
    return PlaitMind.isMind(element) || !!element.layout;
};

const resolveLayoutRelationDirection = (
    board: PlaitBoard,
    relation: LayoutRelation
): LayoutDirection => {
    const layout = MindQueries.getCorrectLayoutByElement(
        board,
        relation.parent
    ) as MindLayoutType;

    if (relation.type === 'sibling') {
        const nextDirection = isIndentedLayout(layout)
            ? isTopLayout(layout)
                ? LayoutDirection.top
                : LayoutDirection.bottom
            : isHorizontalLayout(layout)
            ? LayoutDirection.bottom
            : LayoutDirection.right;

        return relation.order === 'next'
            ? nextDirection
            : getLayoutReverseDirection(nextDirection);
    }

    if (isIndentedLayout(layout) && isLayoutRoot(relation.parent)) {
        return isTopLayout(layout)
            ? LayoutDirection.top
            : LayoutDirection.bottom;
    }

    return getLayoutDirection(
        MindElement.getNode(relation.child),
        isHorizontalLayout(layout)
    );
};

The important distinction for indented layouts is:

  • the direct children of an indented layout root use the top/bottom branch direction;
  • deeper parent-child relationships use the left/right hierarchy direction;
  • sibling order is still derived from the common parent's layout context;
  • a nested node with an explicit layout becomes the owner of the layout used for its own descendants.

These details can remain private to resolveLayoutRelationDirection; they do not need to become several public direction helpers.

Direction derivation by layout

The following examples show why the relationship owner must be explicit.

Horizontal logic layout

For a normal right layout:

parent direction          = left
child direction           = right
previous sibling direction = top
next sibling direction     = bottom

The parent-child axis is horizontal, so siblings use the cross axis.

Upward layout

For an upward layout:

parent direction          = bottom
child direction           = top
previous sibling direction = left
next sibling direction     = right

The same relationship model works without hard-coding left/right = parent/child and top/bottom = sibling.

Right-top indented layout

For the direct children of a right-top indented layout root:

parent direction          = bottom
child direction           = top
previous sibling direction = bottom
next sibling direction     = top

This layout intentionally creates direction conflicts. For the second root child, ArrowDown matches both its parent and its previous sibling. The expected behavior is:

second child + ArrowDown -> previous sibling
first child  + ArrowDown -> parent, because no previous sibling exists

The same applies symmetrically to a bottom-indented layout. This is why the candidate needs a relation field and why target selection must define relation priority instead of returning an arbitrary first match.

Nested rightTopIndented -> right

Consider this tree:

Outer parent: rightTopIndented
  Previous sibling
  Source: layout = right
    Child A
    Child B
  Next sibling

The candidates for Source are derived from different owners:

parent candidate:
  owner = outer parent layout context

previous/next sibling candidates:
  owner = outer parent's rightTopIndented context

child candidates:
  owner = Source's nested right layout

Therefore, the nested right layout changes navigation from Source to its descendants, but does not change how Source navigates to its parent or siblings. Deriving every direction from getCorrectLayout(board, source) cannot represent this distinction.

Standard layout

For a standard root, outgoing child candidates may point in both horizontal directions:

right-side child candidate = right
left-side child candidate  = left

Sibling order is still the visible child order and uses top/bottom navigation. If the visible order is:

[rightA, rightB, leftA, leftB]

then the structural transitions are:

rightB + ArrowDown -> leftA
leftA  + ArrowUp   -> rightB

The left/right direction remains relevant for parent-child navigation and geometry filtering, but should not split the root sibling sequence.

Construct the candidates

const getLayoutNavigationCandidates = (
    board: PlaitBoard,
    source: MindElement
): LayoutNavigationCandidate[] => {
    const candidates: LayoutNavigationCandidate[] = [];
    const parent = getVisibleParent(source);

    if (parent) {
        const incomingDirection = resolveLayoutRelationDirection(board, {
            type: 'parent-child',
            parent,
            child: source
        });

        candidates.push({
            element: parent,
            relation: 'parent',
            direction: getLayoutReverseDirection(incomingDirection)
        });
    }

    getVisibleChildren(board, source).forEach((child) => {
        candidates.push({
            element: child,
            relation: 'child',
            direction: resolveLayoutRelationDirection(board, {
                type: 'parent-child',
                parent: source,
                child
            })
        });
    });

    if (parent) {
        const siblings = getVisibleChildren(board, parent);
        const sourceIndex = siblings.indexOf(source);
        const previousSibling = siblings[sourceIndex - 1];
        const nextSibling = siblings[sourceIndex + 1];

        if (previousSibling) {
            candidates.push({
                element: previousSibling,
                relation: 'previous-sibling',
                direction: resolveLayoutRelationDirection(board, {
                    type: 'sibling',
                    parent,
                    order: 'previous'
                })
            });
        }

        if (nextSibling) {
            candidates.push({
                element: nextSibling,
                relation: 'next-sibling',
                direction: resolveLayoutRelationDirection(board, {
                    type: 'sibling',
                    parent,
                    order: 'next'
                })
            });
        }
    }

    return candidates;
};

For the standard layout, root children should not be filtered by left/right direction when constructing siblings. Keeping the original visible child order allows the expected transition:

last right-side sibling + ArrowDown -> first left-side sibling
first left-side sibling + ArrowUp    -> last right-side sibling

The parent/child candidates still resolve their own left/right direction independently.

Select the target by direction

const resolveLayoutNavigationTarget = (
    candidates: LayoutNavigationCandidate[],
    direction: LayoutDirection,
    previousElement?: MindElement
) => {
    const matches = candidates.filter(
        (candidate) => candidate.direction === direction
    );

    const sibling = matches.find(
        (candidate) =>
            candidate.relation === 'previous-sibling' ||
            candidate.relation === 'next-sibling'
    );
    if (sibling) {
        return sibling.element;
    }

    const parent = matches.find(
        (candidate) => candidate.relation === 'parent'
    );
    if (parent) {
        return parent.element;
    }

    const children = matches.filter(
        (candidate) => candidate.relation === 'child'
    );
    const rememberedChild = children.find(
        (candidate) => candidate.element === previousElement
    );

    return rememberedChild?.element || children[0]?.element;
};

This priority is not an implementation accident. It is the explicit navigation policy for layouts where sibling and parent directions overlap:

matching sibling
    -> matching parent
    -> child matching previousElement
    -> first matching child

The old sibling || parent/child expression happened to encode part of this priority, but it hid the reason. The candidate model keeps candidate discovery independent from target selection and makes the conflict policy testable.

The main lookup then becomes a direct pipeline instead of sibling/parent-child fallback ordering:

export const getNextMindElementByDirection = (
    board: PlaitBoard,
    source: MindElement,
    direction: Direction,
    previousElement?: MindElement
) => {
    const layoutDirection = DirectionToLayoutDirection[direction];
    const candidates = getLayoutNavigationCandidates(board, source);
    const layoutTarget = resolveLayoutNavigationTarget(
        candidates,
        layoutDirection,
        previousElement
    );

    return (
        layoutTarget ||
        getNextMindElementByGeometry(board, source, direction)
    );
};

For a node using right inside rightTopIndented, this produces the intended separation of contexts:

  • parent candidate: resolved from the outer parent-child context;
  • previous/next sibling candidates: resolved from the outer rightTopIndented context;
  • child candidates: resolved from the node's own nested right layout.

Navigation invariants

The implementation should preserve these invariants:

  1. Only visible nodes become layout navigation candidates.
  2. Collapsed descendants are not child candidates.
  3. An incoming edge is resolved from the parent that owns that edge.
  4. Sibling order and sibling directions are resolved from the common parent.
  5. A node's nested layout only owns its outgoing descendants.
  6. Standard root siblings preserve visible child order across the right/left boundary.
  7. Sibling candidates win when sibling and parent directions overlap.
  8. previousElement only chooses among matching child candidates; it does not override sibling or parent navigation.
  9. Geometry is a fallback only after no layout candidate is selected.

This separation also makes failures easier to diagnose:

wrong candidate set
    -> visible tree/relation construction problem

correct relation, wrong direction
    -> layout ownership/direction resolution problem

multiple correct-direction candidates, wrong target
    -> relation priority/history selection problem

no layout candidate, unexpected jump
    -> geometry fallback problem

Scope of layout-direction.ts

To keep this PR focused, we do not need to migrate existing direction handling in point-placement.ts, Node More, DnD, or link drawing now. Those callers have related but different semantics:

  • link drawing resolves a rendered connector direction;
  • Node More resolves an affordance placement direction;
  • DnD resolves a hypothetical insertion direction;
  • keyboard navigation resolves an existing structural relationship.

This PR can add the relation-oriented resolver for navigation while continuing to call the existing low-level getLayoutDirection internally.

As a follow-up, we should audit point-placement.ts#getLayoutDirection, getNodeMoreLayoutDirection, and the DnD direction handling, extract a shared rendered-direction primitive into layout-direction.ts, and migrate only the callers whose semantics are genuinely compatible. That follow-up should preserve existing behavior rather than turning the new module into a universal direction function with feature-specific modes.

Regression coverage

Please add coverage for:

  • rightTopIndented -> right nested layout;
  • navigating from the outer parent into the nested-layout node;
  • navigating from the nested-layout node back to its parent;
  • previous/next sibling navigation at the nested-layout boundary;
  • navigation among descendants inside the nested layout;
  • standard-layout sibling navigation across the right/left branch boundary.

We can review the geometry fallback separately. For now, hasCrossAxisOverlap could be renamed to isInSameNavigationLane so its intent is clearer and it remains isolated from the layout-based candidate model.

@pubuzhixing8

Copy link
Copy Markdown
Collaborator

Thoughts on This PR
I hesitated a lot handling this PR. I usually let Codex review simple PRs, yet this one has complex legacy logic that needs full context comprehension. Overusing AI makes me lazy, and I doubted if deep manual analysis was worth the time.
After consideration, I spent around 3 hours manually auditing the original layout-direction logic and collaborating with LLMs to finalize refined implementation plans. I streamlined the logic and standardized semantic naming. To avoid scope creep, secondary refactors triggered by layout-direction extraction will be moved to Phase 2. We only finished layout-based navigation here; geometry-driven navigation is a follow-up task.
Takeaway: Core module refactors require heavy manual work. AI outputs are rough drafts that need thorough human tuning, as they miss legacy context and long-term architecture considerations.

@nightt5879

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed review. I pushed 3abcb42b and implemented the layout-navigation phase around relationship ownership.

  • Added a focused layout-direction.ts resolver for parent-child and sibling relationships.
  • Build visible parent, child, previous-sibling, and next-sibling candidates before selecting a target.
  • Resolve conflicts explicitly as sibling → parent → remembered child → first child.
  • Preserve standard sibling order across the right/left branch boundary.
  • Keep geometry isolated as the fallback; no new geometry policy or cross-feature direction refactor is included here.

One deliberate refinement from the implementation sketch is that sibling direction reads the common parent's final rendered up / left orientation. A nested subtree can be mirrored by its outer layout. In the rightTopIndented -> right case, using only the declared right layout made a three-sibling sequence loop from A-2 back to A-1 instead of continuing to the visually previous node. The rendered orientation keeps direction ownership with the common parent while also respecting the legacy layout transform.

Regression coverage now includes:

  • entering the nested-layout node from the outer parent and navigating back;
  • previous/next sibling navigation at the nested-layout boundary;
  • descendant navigation in a mirrored three-sibling nested layout;
  • a symmetric horizontal mirror case using left -> downward;
  • standard-layout navigation across the right/left branch boundary.

Validation:

  • focused hotkey specs: 24 passed;
  • full mind suite: 45 passed;
  • build:mind and build:demo passed;
  • targeted ESLint, Prettier, and diff checks passed;
  • manual paths verified: A-1 ↑ A-2 ↑ 111, reverse with ArrowDown, and D ↓ E ↑ D.

I agree that broader rendered-direction reuse and geometry-driven navigation belong in Phase 2. @pubuzhixing8, could you please take another look?

@nightt5879 nightt5879 requested a review from pubuzhixing8 July 14, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants