Skip to content

Conversation

@eliandoran
Copy link
Contributor

@eliandoran eliandoran commented Dec 8, 2025

Closes #3754.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @eliandoran, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new breadcrumb navigation feature to enhance user experience by providing clear hierarchical context within the application. It involves the creation of a dedicated Breadcrumb React component, its integration into the desktop layout, and associated styling. Additionally, the PR refines link creation logic for better handling of undefined titles, improves the useChildNotes hook for robustness, and updates ESLint configuration to cover TypeScript React files.

Highlights

  • New Breadcrumb Navigation Component: A new Breadcrumb React component has been introduced, providing clear hierarchical navigation within the application. It dynamically displays the current note's path and offers interactive dropdowns for navigating parent and sibling notes.
  • Desktop Layout Integration: The Breadcrumb component is now integrated into the desktop layout, replacing the previous title row in the main note wrapper. The original title row content (note icon and title) has been repositioned within the ContentHeader.
  • Refined Link Creation Logic: The createLink service now uses a more precise undefined check for the linkTitle option, ensuring that null or empty string values are not mistakenly treated as undefined, leading to more predictable link title generation.
  • Sticky Ribbon Styling: The Ribbon component has received new CSS styling to make it sticky at the top of the content area, improving its visibility and accessibility during scrolling.
  • Robust useChildNotes Hook: The useChildNotes hook has been enhanced to gracefully handle cases where parentNoteId might be undefined, preventing potential errors and making the component more resilient.
  • ESLint Configuration Update: The ESLint configuration has been updated to include .tsx files, ensuring that TypeScript React components are properly linted and adhere to code quality standards.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new breadcrumb component, which is a great addition for improving navigation. The implementation is solid, and it's well-integrated into the desktop layout. I've identified a few areas for improvement, including a potential issue with an incorrect file extension in an import and a couple of opportunities to refactor the new Breadcrumb.tsx component for better clarity and maintainability. Overall, this is a well-executed feature.

import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import Breadcrumb from "../widgets/Breadcrumb.jsx";

This comment was marked as off-topic.

Comment on lines +155 to +165
function buildNotePaths(notePathArray: string[] | undefined) {
if (!notePathArray) return [];

let prefix = "";
const output: string[] = [];
for (const notePath of notePathArray) {
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
}
return output;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This function can be written more declaratively and concisely by using the map method. This would improve readability and maintainability.

function buildNotePaths(notePathArray: string[] | undefined) {
    if (!notePathArray) {
        return [];
    }

    return notePathArray.map((_, i) => notePathArray.slice(0, i + 1).join('/'));
}

@eliandoran eliandoran marked this pull request as ready for review December 9, 2025 10:31
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Dec 9, 2025
@eliandoran eliandoran requested a review from Copilot December 9, 2025 10:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a breadcrumb navigation component to the Trilium Notes desktop layout, replacing the title row with a breadcrumb that shows the note's hierarchical path. The implementation includes floating header behavior and adjusts the positioning of ribbon and floating buttons to work with the new layout.

Key Changes:

  • Implements a new breadcrumb component with collapsible paths and dropdown navigation for child notes
  • Refactors content header to float on scroll with dynamic positioning via CSS custom properties
  • Adjusts floating buttons positioning to account for dynamic header heights
  • Moves the title row from top-level to inside the scrolling content area

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
apps/client/src/widgets/Breadcrumb.tsx New breadcrumb component with path navigation and dropdown menus
apps/client/src/widgets/Breadcrumb.css Styling for the breadcrumb component with flexible layout
apps/client/src/layouts/desktop_layout.tsx Replaces title row with breadcrumb, moves title inside scrolling container
apps/client/src/widgets/containers/content_header.ts Adds floating behavior on scroll with CSS custom property updates
apps/client/src/widgets/containers/content_header.css Styles for floating header with sticky positioning
apps/client/src/widgets/FloatingButtons.tsx Removes event-based positioning in favor of CSS variables
apps/client/src/widgets/FloatingButtons.css Updates positioning to use CSS custom properties for dynamic height
apps/client/src/widgets/ribbon/Ribbon.tsx Adds height tracking via ResizeObserver and CSS custom properties
apps/client/src/widgets/ribbon/style.css Makes ribbon sticky when content header floats
apps/client/src/widgets/react/hooks.tsx Updates useChildNotes to handle undefined parent IDs
apps/client/src/widgets/react/react_utils.tsx Makes separator parameter in joinElements accept ComponentChild
apps/client/src/services/link.ts Fixes title handling to allow empty strings (was treating falsy as undefined)
apps/client/src/components/app_context.ts Removes deprecated contentSafeMarginChanged event
eslint.config.mjs Adds .tsx files to ESLint configuration
Various CSS files Minor whitespace and styling fixes for layout consistency

Comment on lines +72 to +75
if (this.isFloating || this.parentElement!.scrollTop === 0) {
parentEl!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
parentEl!.style.removeProperty("--content-header-height");
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Potential null pointer exception: parentEl could be null if .closest() doesn't find a matching ancestor, but the code uses parentEl! (non-null assertion) at lines 73 and 75. This will throw a runtime error if the note-split container is not found.

Add a null check before accessing parentEl:

const parentEl = this.parentElement?.closest<HTMLDivElement>(".note-split");
if (!parentEl) return;
Suggested change
if (this.isFloating || this.parentElement!.scrollTop === 0) {
parentEl!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
parentEl!.style.removeProperty("--content-header-height");
if (!parentEl) return;
if (this.isFloating || this.parentElement!.scrollTop === 0) {
parentEl.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
parentEl.style.removeProperty("--content-header-height");

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +169
let prefix = "";
const output: string[] = [];
for (const notePath of notePathArray) {
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
}
return output;
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The function buildNotePaths mutates the prefix variable on each iteration, which could be simplified using map with slice:

function buildNotePaths(notePathArray: string[] | undefined) {
    if (!notePathArray) return [];
    
    return notePathArray.map((_, index) => 
        notePathArray.slice(0, index + 1).join("/")
    );
}

This approach is more functional and easier to understand.

Suggested change
let prefix = "";
const output: string[] = [];
for (const notePath of notePathArray) {
output.push(`${prefix}${notePath}`);
prefix += `${notePath}/`;
}
return output;
return notePathArray.map((_, index) =>
notePathArray.slice(0, index + 1).join("/")
);

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 59
export default function Breadcrumb() {
const { note, noteContext } = useNoteContext();
const notePath = buildNotePaths(noteContext?.notePathArray);

return (
<div className="breadcrumb">
{notePath.length > COLLAPSE_THRESHOLD ? (
<>
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} activeNotePath={noteContext?.notePath ?? ""} />
}
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />
</Fragment>
))}
<BreadcrumbCollapsed items={notePath.slice(INITIAL_ITEMS, -FINAL_ITEMS)} noteContext={noteContext} />
{notePath.slice(-FINAL_ITEMS).map((item, index) => (
<Fragment key={item}>
<BreadcrumbSeparator notePath={notePath[notePath.length - FINAL_ITEMS - (1 - index)]} activeNotePath={item} noteContext={noteContext} />
<BreadcrumbItem notePath={item} activeNotePath={noteContext?.notePath ?? ""} />
</Fragment>
))}
</>
) : (
notePath.map((item, index) => (
<Fragment key={item}>
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} activeNotePath={noteContext?.notePath ?? ""} />
}
{(index < notePath.length - 1 || note?.hasChildren()) &&
<BreadcrumbSeparator notePath={item} activeNotePath={notePath[index + 1]} noteContext={noteContext} />}
</Fragment>
))
)}
</div>
);
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The new Breadcrumb component lacks test coverage. Given that the codebase has comprehensive tests for other widgets (e.g., apps/client/src/widgets/collections/ components have .spec.ts files), this component should have tests covering:

  • Path building logic with various note path lengths
  • Collapsed breadcrumb behavior when exceeding COLLAPSE_THRESHOLD
  • Dropdown content rendering for child notes
  • Edge cases like empty paths or missing notes

Copilot uses AI. Check for mistakes.
@eliandoran eliandoran changed the title Feature/breadcrumb Add breadcrumbs to navigation Dec 9, 2025
@eliandoran eliandoran added this to the v1.0.0 milestone Dec 9, 2025
@eliandoran eliandoran merged commit e688f2c into main Dec 9, 2025
10 of 11 checks passed
@eliandoran eliandoran deleted the feature/breadcrumb branch December 9, 2025 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(Feature request) Note Path as a breadcrumb navigation

2 participants