Skip to content

feat: scope tree helper hover highlight + wider clickable rails#61

Closed
dpshde wants to merge 1 commit into
cameronapak:mainfrom
dpshde:feat/tree-helpers
Closed

feat: scope tree helper hover highlight + wider clickable rails#61
dpshde wants to merge 1 commit into
cameronapak:mainfrom
dpshde:feat/tree-helpers

Conversation

@dpshde

@dpshde dpshde commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Two tree-helper improvements:

  1. Scoped hover highlight - Previously, hovering a guide rail highlighted the entire depth column (all branches at that indent level). Now scoped using the specific ancestor ownerId from railOwnerIds, so only the segments under the hovered collapsible ancestor light up. Normal guides stay flat.

  2. Wider clickable rails - RAIL_HIT_WIDTH_PX 12px to 20px, making the vertical guide lines much easier to click for collapse/expand without changing visuals.

  3. Rail centering fix - 1px guide lines now center precisely on bullet-dot axes (shifted 0.5px to account for sub-pixel rendering).

Screenshots

Base outline with tree guides:

tree-base

Hovering "Website redesign" rail — only its 3 children highlight:

tree-hover-web

Hovering "Projects" rail — all 7 descendants highlight:

tree-hover-projects

Summary by CodeRabbit

  • New Features

    • Added visible guide rails beside outline rows to better show hierarchy and hover targets.
    • Collapse/expand now works from rail controls across branches and mirrors, with improved flashing feedback when items change state.
  • Bug Fixes

    • Updated collapse behavior in mirrored and collapsed views so only the intended content is affected.
    • Refined spacing, hover highlighting, and touch behavior for a cleaner outline interaction.

… entire column)

- Use ownerId to tag only descendant rows for .rail-hovered
- Highlight only the relevant segments under the hovered ancestor
- Keep normal guides flat

Also includes supporting railOwnerIds propagation and e2e updates.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces row chevron collapse affordances with hoverable ancestor guide rails: VisibleRow/buildVisibleRows gain railOwnerIds, threaded through OutlineEditor/OutlineRow/MirrorMissingRow to render RailToggles/CollapsedRailToggle with hover highlighting and new CSS. Chevron icons and related styles are removed. Tests updated accordingly. Also adds an unrelated AuthGate bypass flag.

Changes

Guide Rail Interaction Feature

Layer / File(s) Summary
railOwnerIds data model
src/data/visible-order.ts, src/data/visible-order.test.ts
VisibleRow adds railOwnerIds: string[], populated and threaded through the recursive buildVisibleRows walk for normal, broken-mirror, and capped-mirror rows, with unit tests verifying ownership chains.
Chevron UI removal
src/components/OutlineNode.tsx, src/components/OutlineRow.tsx
Removes ChevronRight imports and chevron glyph rendering from collapse-toggle buttons in both components.
Rail components and wiring
src/components/OutlineRow.tsx, src/components/OutlineEditor.tsx
OutlineRowProps and MirrorMissingRow gain railOwnerIds; data-rail-owners/data-collapsed-branch attributes are added; new RailToggles and CollapsedRailToggle components render per-owner rail buttons and implement hover highlighting via CSS variables and rail-hovered class toggling.
Rail and connector styling
src/styles.css
Adds --tree-guide/--tree-guide-hover variables, per-depth gradient connectors, .rail-toggle/.collapsed-rail-toggle hit targets, hover highlight rule, collapsed-branch padding, and adjusts .collapse-toggle geometry and reduced-motion/touch-hitbox rules.
E2E test updates
e2e/collapse-flash.spec.ts, e2e/mirrors.spec.ts
Updates collapse/expand interactions to click rail toggles (.rail-toggle[data-rail-owner-id="..."]) instead of row chevrons, keeping flash/visibility assertions.

Auth Bypass Flag

Layer / File(s) Summary
AuthGate env bypass
src/routes/__root.tsx
Adds early-return in AuthGate that skips session gating and renders children when VITE_BYPASS_AUTH is set.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RailToggles
  participant OutlineNode
  participant OutlineRow
  User->>RailToggles: mouseover rail button
  RailToggles->>OutlineNode: set --hovered-rail-x, add rail-hovered class
  User->>RailToggles: click rail toggle
  RailToggles->>OutlineRow: toggle collapsed state
  OutlineRow-->>OutlineNode: re-render collapsed/expanded rows
  User->>RailToggles: mouseleave list
  RailToggles->>OutlineNode: remove rail-hovered class
Loading

Possibly related PRs

  • cameronapak/dotflowy#44: Extends the same virtualized flat-row architecture (VisibleRow/buildVisibleRows and OutlineRow) that this PR modifies to add railOwnerIds.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: scoped rail hover highlighting and wider clickable rails.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/routes/__root.tsx (1)

85-91: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Guard the auth bypass behind dev-only builds.

VITE_BYPASS_AUTH skips the session gate entirely, and because VITE_ env vars are baked into the client bundle, a stray staging/prod build setting would expose the app without auth. Restrict this to import.meta.env.DEV (or another explicit non-prod check) so the bypass can’t be enabled in shipped builds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/__root.tsx` around lines 85 - 91, The AuthGate bypass currently
relies only on VITE_BYPASS_AUTH, which can be baked into shipped client builds;
update AuthGate to allow the bypass only when import.meta.env.DEV (or an
equivalent explicit non-production check) is also true, so the session gate
cannot be skipped in staging/prod. Keep the change localized to the AuthGate
logic and preserve the existing useSession, isPending, and AuthScreen flow for
normal authenticated access.
src/components/OutlineNode.tsx (1)

293-304: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recursive fallback needs a visible collapse affordance.

The non-virtualized path still uses the invisible .collapse-toggle, but it never gets the rail-based controls the windowed path uses (data-depth/rail toggles), so collapse/expand is only hinted at by the subtle collapsed-bullet styling. That makes the fallback harder to discover when virtualized is off.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/OutlineNode.tsx` around lines 293 - 304, The non-virtualized
row in OutlineNode still relies on the invisible collapse-toggle and only subtle
collapsed-bullet styling, so add a visible collapse/expand affordance in the
fallback path. Update OutlineNode’s non-virtualized rendering to mirror the
windowed path’s rail-based controls using the existing node state and props such
as hasChildren, effectiveCollapsed, data-depth, and commands.onToggleCollapsed
so the toggle is clearly discoverable when virtualized is off.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/OutlineRow.tsx`:
- Around line 618-697: `RailToggles` and `CollapsedRailToggle` both contain the
same `setRailHover` hover-highlighting logic, so extract that shared behavior
into a helper (for example, `highlightRailForOwner` or a small hook) that
handles the `.outline-list` lookup, one-time `mouseleave` cleanup,
`--hovered-rail-x` updates, and `data-rail-owners` scoping. Then update both
components’ `onMouseEnter` handlers to call the shared helper with the event,
`ownerId`, and `globalX`, keeping the existing hover behavior identical while
removing duplication.
- Around line 51-52: The widened rail hit area in OutlineRow is overlapping the
collapse-toggle touch target, so gutter clicks can be captured by the parent row
instead of the current row. Adjust the rail hitbox logic around RAIL_CENTER_X_PX
and RAIL_HIT_WIDTH_PX, and/or lower the rail’s effective hit area or z-index so
it no longer covers the collapse-toggle area; verify the .rail-toggle layering
still allows the intended row-specific collapse action to receive clicks.

---

Outside diff comments:
In `@src/components/OutlineNode.tsx`:
- Around line 293-304: The non-virtualized row in OutlineNode still relies on
the invisible collapse-toggle and only subtle collapsed-bullet styling, so add a
visible collapse/expand affordance in the fallback path. Update OutlineNode’s
non-virtualized rendering to mirror the windowed path’s rail-based controls
using the existing node state and props such as hasChildren, effectiveCollapsed,
data-depth, and commands.onToggleCollapsed so the toggle is clearly discoverable
when virtualized is off.

In `@src/routes/__root.tsx`:
- Around line 85-91: The AuthGate bypass currently relies only on
VITE_BYPASS_AUTH, which can be baked into shipped client builds; update AuthGate
to allow the bypass only when import.meta.env.DEV (or an equivalent explicit
non-production check) is also true, so the session gate cannot be skipped in
staging/prod. Keep the change localized to the AuthGate logic and preserve the
existing useSession, isPending, and AuthScreen flow for normal authenticated
access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba8793b5-aaf3-48e1-a547-dba597abe3a1

📥 Commits

Reviewing files that changed from the base of the PR and between d3202e8 and d1fc738.

📒 Files selected for processing (9)
  • e2e/collapse-flash.spec.ts
  • e2e/mirrors.spec.ts
  • src/components/OutlineEditor.tsx
  • src/components/OutlineNode.tsx
  • src/components/OutlineRow.tsx
  • src/data/visible-order.test.ts
  • src/data/visible-order.ts
  • src/routes/__root.tsx
  • src/styles.css

Comment on lines +51 to +52
const RAIL_CENTER_X_PX = 14;
const RAIL_HIT_WIDTH_PX = 20;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "\.collapse-toggle\s*{" -A15 src/styles.css
rg -n "\.rail-toggle\s*{" -A15 src/styles.css

Repository: cameronapak/dotflowy

Length of output: 362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'src/components/OutlineRow.tsx' 'src/styles.css'
wc -l src/components/OutlineRow.tsx src/styles.css
sed -n '1,220p' src/components/OutlineRow.tsx
printf '\n--- styles ---\n'
rg -n '\.collapse-toggle\s*\{' -A20 src/styles.css
printf '\n---\n'
rg -n '\.rail-toggle\s*\{' -A20 src/styles.css

Repository: cameronapak/dotflowy

Length of output: 10407


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '220,460p' src/components/OutlineRow.tsx
printf '\n---\n'
sed -n '460,720p' src/components/OutlineRow.tsx

Repository: cameronapak/dotflowy

Length of output: 18125


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\.outline-row\s*\{|\.outline-node\s*\{|\.touch-hitbox\s*\{|\.collapse-toggle|\.rail-toggle|\.collapsed-rail-toggle' -A18 src/styles.css
printf '\n---\n'
sed -n '720,860p' src/components/OutlineRow.tsx

Repository: cameronapak/dotflowy

Length of output: 6436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\.touch-hitbox\b|::before' -A24 src/styles.css

Repository: cameronapak/dotflowy

Length of output: 5011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\.touch-hitbox\b' -A20 src/styles.css

Repository: cameronapak/dotflowy

Length of output: 912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'touch-hitbox' -S .

Repository: cameronapak/dotflowy

Length of output: 902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1060,1095p' src/styles.css

Repository: cameronapak/dotflowy

Length of output: 1475


RAIL_HIT_WIDTH_PX overlaps the row collapse target

The widened rail hitbox reaches into the collapse-toggle’s touch area, and .rail-toggle sits above it (z-index: 3), so clicks near the gutter can trigger the parent collapse action instead of the current row’s collapse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/OutlineRow.tsx` around lines 51 - 52, The widened rail hit
area in OutlineRow is overlapping the collapse-toggle touch target, so gutter
clicks can be captured by the parent row instead of the current row. Adjust the
rail hitbox logic around RAIL_CENTER_X_PX and RAIL_HIT_WIDTH_PX, and/or lower
the rail’s effective hit area or z-index so it no longer covers the
collapse-toggle area; verify the .rail-toggle layering still allows the intended
row-specific collapse action to receive clicks.

Comment on lines +618 to +697
function RailToggles({
depth,
ownerIds,
onCollapse,
}: {
depth: number;
ownerIds: string[];
onCollapse: (ownerId: string) => void;
}) {
if (ownerIds.length === 0) return null;

const setRailHover = (e: React.MouseEvent, ownerId: string, globalX: number) => {
const list = (e.currentTarget as HTMLElement).closest<HTMLElement>(".outline-list");
if (!list) return;

const clearAll = () => {
list.style.removeProperty("--hovered-rail-x");
list.querySelectorAll<HTMLElement>(".outline-node").forEach((li) =>
li.classList.remove("rail-hovered")
);
delete list.dataset.railHoverListener;
};

// Attach a one-time clearer when the pointer leaves the whole list area.
// This keeps the highlight stable while the mouse travels between
// vertically adjacent segments of the same rail.
if (!list.dataset.railHoverListener) {
list.addEventListener("mouseleave", clearAll, { once: true });
list.dataset.railHoverListener = "1";
}

list.style.setProperty("--hovered-rail-x", `${globalX}px`);

// Scope the highlight to only the "collapsable part": rows whose ancestry
// includes this specific ownerId (i.e. descendants under that ancestor).
// This avoids lighting the entire depth column across sibling branches.
list.querySelectorAll<HTMLElement>(".outline-node").forEach((li) => {
const rowOwners = (li.dataset.railOwners || "").split(",").filter(Boolean);
if (rowOwners.includes(ownerId)) {
li.classList.add("rail-hovered");
} else {
li.classList.remove("rail-hovered");
}
});
};

return (
<>
{ownerIds.map((ownerId, railIndex) => (
<button
key={`${railIndex}:${ownerId}`}
type="button"
className="rail-toggle"
data-rail-owner-id={ownerId}
aria-label="Collapse branch"
tabIndex={-1}
style={{
left:
RAIL_CENTER_X_PX +
railIndex * INDENT_PX -
depth * INDENT_PX -
RAIL_HIT_WIDTH_PX / 2,
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation();
onCollapse(ownerId);
}}
onMouseEnter={(e) => {
const globalX = RAIL_CENTER_X_PX + railIndex * INDENT_PX;
setRailHover(e, ownerId, globalX);
}}
/>
))}
</>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the duplicated setRailHover hover logic.

RailToggles and CollapsedRailToggle each implement a nearly identical setRailHover closure (list lookup, one-time mouseleave cleanup, --hovered-rail-x update, and data-rail-owners scoping). Extracting a shared helper (e.g., useRailHover or a module-level function taking the event/ownerId/globalX) would remove ~30 lines of duplicated logic and avoid future drift between the two copies.

♻️ Sketch of a shared helper
+function highlightRailForOwner(e: React.MouseEvent, ownerId: string, globalX: number) {
+  const list = (e.currentTarget as HTMLElement).closest<HTMLElement>(".outline-list");
+  if (!list) return;
+  const clearAll = () => {
+    list.style.removeProperty("--hovered-rail-x");
+    list.querySelectorAll<HTMLElement>(".outline-node").forEach((li) =>
+      li.classList.remove("rail-hovered")
+    );
+    delete list.dataset.railHoverListener;
+  };
+  if (!list.dataset.railHoverListener) {
+    list.addEventListener("mouseleave", clearAll, { once: true });
+    list.dataset.railHoverListener = "1";
+  }
+  list.style.setProperty("--hovered-rail-x", `${globalX}px`);
+  list.querySelectorAll<HTMLElement>(".outline-node").forEach((li) => {
+    const owners = (li.dataset.railOwners || "").split(",").filter(Boolean);
+    li.classList.toggle("rail-hovered", owners.includes(ownerId));
+  });
+}

Both RailToggles and CollapsedRailToggle would then call highlightRailForOwner(e, ownerId, globalX) in their onMouseEnter.

Also applies to: 699-762

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/OutlineRow.tsx` around lines 618 - 697, `RailToggles` and
`CollapsedRailToggle` both contain the same `setRailHover` hover-highlighting
logic, so extract that shared behavior into a helper (for example,
`highlightRailForOwner` or a small hook) that handles the `.outline-list`
lookup, one-time `mouseleave` cleanup, `--hovered-rail-x` updates, and
`data-rail-owners` scoping. Then update both components’ `onMouseEnter` handlers
to call the shared helper with the event, `ownerId`, and `globalX`, keeping the
existing hover behavior identical while removing duplication.

@dpshde dpshde closed this Jul 6, 2026
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.

1 participant