Compare commits

...

6 Commits

Author SHA1 Message Date
Elian Doran
474228b630
style(client): remove bottom border & box-shadow for content header 2025-12-09 08:22:51 +02:00
Elian Doran
0805e077a1
feat(ribbon): basic implementation for scroll pinning 2025-12-09 08:18:27 +02:00
Elian Doran
6b059a9a75
feat(ribbon): context menu for root item 2025-12-09 08:01:52 +02:00
Elian Doran
7377e4e34d
chore(ribbon): improve paddings slightly 2025-12-09 07:58:59 +02:00
Elian Doran
6fac947d9c
chore(ribbon): address requested changes 2025-12-09 07:50:21 +02:00
Elian Doran
5973e5ca26
chore(ribbon): remove label for the root entirely 2025-12-09 07:46:46 +02:00
7 changed files with 76 additions and 11 deletions

View File

@ -122,6 +122,7 @@ export default class DesktopLayout {
.css("height", "30px")
.css("min-height", "30px")
.css("align-items", "center")
.css("padding", "10px")
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
.child(<Breadcrumb />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)

View File

@ -4,8 +4,8 @@
.component.breadcrumb {
contain: none;
margin: 0 10px;
display: flex;
margin: 0;
align-items: center;
font-size: 0.9em;
gap: 0.25em;

View File

@ -11,6 +11,7 @@ import { FormListItem } from "./react/FormList";
import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks";
import Icon from "./react/Icon";
import NoteLink from "./react/NoteLink";
import link_context_menu from "../menus/link_context_menu";
const COLLAPSE_THRESHOLD = 5;
const INITIAL_ITEMS = 2;
@ -26,7 +27,7 @@ export default function Breadcrumb() {
<>
{notePath.slice(0, INITIAL_ITEMS).map((item, index) => (
<Fragment key={item}>
{index === 0 && notePath.length > 1
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} activeNotePath={noteContext?.notePath ?? ""} />
}
@ -44,7 +45,7 @@ export default function Breadcrumb() {
) : (
notePath.map((item, index) => (
<Fragment key={item}>
{index === 0 && notePath.length > 1
{index === 0
? <BreadcrumbRoot noteContext={noteContext} />
: <BreadcrumbItem notePath={item} activeNotePath={noteContext?.notePath ?? ""} />
}
@ -67,6 +68,10 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined
icon={note.getIcon()}
text={title ?? ""}
onClick={() => noteContext?.setNote("root")}
onContextMenu={(e) => {
e.preventDefault();
link_context_menu.openContextMenu(note.noteId, e);
}}
/>
);
}
@ -99,8 +104,8 @@ function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePa
function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) {
const notePathComponents = notePath.split("/");
const notePathPrefix = notePathComponents.join("/"); // last item was removed already.
const parentNoteId = notePathComponents.length > 1 ? notePathComponents.pop() : "root";
const notePathPrefix = notePathComponents.join("/");
const parentNoteId = notePathComponents.at(-1);
const childNotes = useChildNotes(parentNoteId);
return (

View File

@ -0,0 +1,17 @@
.content-header-widget {
position: relative;
transition: position 0.3s ease, box-shadow 0.3s ease, z-index 0.3s ease;
z-index: 8;
}
.content-header-widget.floating {
position: sticky;
top: 0;
z-index: 11;
background-color: var(--main-background-color, #fff);
}
/* Ensure content inside doesn't get affected by the floating state */
.content-header-widget > * {
transition: inherit;
}

View File

@ -2,15 +2,19 @@ import { EventData } from "../../components/app_context";
import BasicWidget from "../basic_widget";
import Container from "./container";
import NoteContext from "../../components/note_context";
import "./content_header.css";
export default class ContentHeader extends Container<BasicWidget> {
noteContext?: NoteContext;
thisElement?: HTMLElement;
parentElement?: HTMLElement;
resizeObserver: ResizeObserver;
currentHeight: number = 0;
currentSafeMargin: number = NaN;
previousScrollTop: number = 0;
isFloating: boolean = false;
scrollThreshold: number = 10; // pixels before triggering float
constructor() {
super();
@ -35,7 +39,36 @@ export default class ContentHeader extends Container<BasicWidget> {
this.thisElement = this.$widget.get(0)!;
this.resizeObserver.observe(this.thisElement);
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
this.parentElement.addEventListener("scroll", this.updateScrollState.bind(this), { passive: true });
}
updateScrollState() {
const currentScrollTop = this.parentElement!.scrollTop;
const isScrollingUp = currentScrollTop < this.previousScrollTop;
const hasMovedEnough = Math.abs(currentScrollTop - this.previousScrollTop) > this.scrollThreshold;
if (hasMovedEnough) {
this.setFloating(isScrollingUp);
this.previousScrollTop = currentScrollTop;
}
this.updateSafeMargin();
}
setFloating(shouldFloat: boolean) {
if (shouldFloat !== this.isFloating) {
this.isFloating = shouldFloat;
if (shouldFloat) {
this.$widget.addClass("floating");
// Set CSS variable so ribbon can position itself below the floating header
this.parentElement!.style.setProperty("--content-header-height", `${this.currentHeight}px`);
} else {
this.$widget.removeClass("floating");
// Reset CSS variable when header is not floating
this.parentElement!.style.removeProperty("--content-header-height");
}
}
}
updateSafeMargin() {
@ -60,4 +93,4 @@ export default class ContentHeader extends Container<BasicWidget> {
}
}
}
}

View File

@ -17,7 +17,6 @@
.info-bar-subtle {
color: var(--muted-text-color);
background: var(--main-background-color);
border-bottom: 1px solid var(--main-border-color);
margin-block: 0;
padding-inline: 22px;
}
}

View File

@ -1,5 +1,15 @@
.ribbon-container {
margin-bottom: 5px;
position: relative;
transition: position 0.3s ease, z-index 0.3s ease, top 0.3s ease;
z-index: 8;
}
/* When content header is floating, ribbon sticks below it */
.scrolling-container:has(.content-header-widget.floating) .ribbon-container {
position: sticky;
top: var(--content-header-height, 100px);
z-index: 10;
}
.ribbon-top-row {
@ -404,4 +414,4 @@ body[dir=rtl] .attribute-list-editor {
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
/* #endregion */
/* #endregion */