diff --git a/frontend/src/documents/SelectionAssignmentMenu.jsx b/frontend/src/documents/SelectionAssignmentMenu.jsx
index 7f2742e..e5ff17b 100644
--- a/frontend/src/documents/SelectionAssignmentMenu.jsx
+++ b/frontend/src/documents/SelectionAssignmentMenu.jsx
@@ -181,6 +181,7 @@ const SelectionAssignmentMenu = ({
ref={menuRef}
style={menuStyle || undefined}
role="menu"
+ data-floating-position
>
{total ? (
options.map((option) => {
diff --git a/frontend/src/styles/base/theme.css b/frontend/src/styles/base/theme.css
index 6b7db9f..44bbe35 100644
--- a/frontend/src/styles/base/theme.css
+++ b/frontend/src/styles/base/theme.css
@@ -14,6 +14,7 @@
--surface-subtle: oklch(0.96 0.002 calc(var(--neutral-hue) + var(--surface-hue-shift)));
--fg: oklch(0.32 0.005 calc(var(--neutral-hue) + var(--foreground-hue-offset)));
--muted: oklch(0.54 0.008 calc(var(--neutral-hue) + var(--foreground-hue-offset)));
+ --muted-subtle: color-mix(in oklch, var(--muted) 55%, var(--border));
--sidebar-fg: oklch(0.56 0.007 calc(var(--neutral-hue) + var(--foreground-hue-offset)));
--border: oklch(0.92 0.002 calc(var(--neutral-hue) + var(--foreground-hue-offset)));
@@ -112,6 +113,7 @@
--surface-subtle: oklch(0.23 0.012 calc(var(--dark-neutral-hue) + var(--surface-hue-shift)));
--fg: oklch(0.89 0.015 var(--dark-foreground-hue));
--muted: oklch(0.72 0.02 var(--dark-foreground-hue));
+ --muted-subtle: color-mix(in oklch, var(--muted) 45%, var(--border));
--sidebar-fg: oklch(0.78 0.02 var(--dark-foreground-hue));
--border: oklch(0.33 0.01 var(--dark-foreground-hue));
@@ -189,6 +191,7 @@
--surface-subtle: oklch(0.23 0.012 calc(var(--dark-neutral-hue) + var(--surface-hue-shift)));
--fg: oklch(0.89 0.015 var(--dark-foreground-hue));
--muted: oklch(0.72 0.02 var(--dark-foreground-hue));
+ --muted-subtle: color-mix(in oklch, var(--muted) 45%, var(--border));
--sidebar-fg: oklch(0.78 0.02 var(--dark-foreground-hue));
--border: oklch(0.33 0.01 var(--dark-foreground-hue));
diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css
index 9ec4119..1afcd0e 100644
--- a/frontend/src/styles/detail/detail-panels.css
+++ b/frontend/src/styles/detail/detail-panels.css
@@ -80,6 +80,8 @@
}
.quick-add {
+ --quick-add-menu-offset: 0.35rem;
+ --quick-add-menu-min-width: 220px;
display: inline-flex;
align-items: center;
position: relative;
@@ -128,10 +130,6 @@
gap: 0.25rem;
}
-.quick-add__menu {
- padding: 0.25rem 0;
-}
-
.quick-add__form {
display: flex;
align-items: center;
@@ -196,8 +194,8 @@
.selection-assignment__header input:focus-visible {
outline: none;
- border-color: var(--selection-border);
- box-shadow: 0 0 0 2px color-mix(in oklch, var(--selection) 25%, transparent);
+ border-color: color-mix(in oklch, var(--accent) 60%, transparent);
+ box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent) 35%, transparent);
}
.selection-assignment__list {
diff --git a/frontend/src/styles/documents/controls.css b/frontend/src/styles/documents/controls.css
index 8c5c8ac..24642cd 100644
--- a/frontend/src/styles/documents/controls.css
+++ b/frontend/src/styles/documents/controls.css
@@ -44,6 +44,10 @@
position: relative;
}
+.documents-sort__quickmenu {
+ --quick-add-menu-min-width: 200px;
+}
+
.documents-sort__trigger {
display: inline-flex;
align-items: center;
@@ -105,4 +109,3 @@
width: 1.1rem;
height: 1.1rem;
}
-
diff --git a/frontend/src/styles/documents/panel-sections.css b/frontend/src/styles/documents/panel-sections.css
index de07a4f..3899f0c 100644
--- a/frontend/src/styles/documents/panel-sections.css
+++ b/frontend/src/styles/documents/panel-sections.css
@@ -84,19 +84,36 @@
cursor: default;
}
+.breadcrumb-trail--measure .breadcrumb-trail__link,
+.breadcrumb-trail--measure .breadcrumb-trail__ellipsis-button {
+ max-width: none;
+ overflow: visible;
+}
+
+button.breadcrumb-trail__link,
+.breadcrumb-trail__ellipsis-button {
+ padding: 0.25rem;
+ background: none;
+ border-radius: 0.25rem;
+}
+
.breadcrumb-trail__link:not(.is-current) {
cursor: pointer;
color: var(--accent);
}
.breadcrumb-trail__link:not(.is-current):hover,
-.breadcrumb-trail__link:not(.is-current):focus-visible {
+.breadcrumb-trail__link:not(.is-current):focus-visible,
+.panel-header .breadcrumb-trail__link:not(.is-current):hover,
+.panel-header .breadcrumb-trail__link:not(.is-current):focus-visible {
text-decoration: underline;
+ background: var(--accent-soft);
+ color: var(--accent);
}
.breadcrumb-trail__separator {
- color: var(--muted);
- margin: 0 0.2rem;
+ color: var(--muted-subtle);
+ margin: 0;
}
.breadcrumb-trail__ellipsis {
@@ -130,4 +147,3 @@
.panel-section__body.scrollable {
overflow-y: auto;
}
-
diff --git a/frontend/src/styles/layout/structure.css b/frontend/src/styles/layout/structure.css
index 4f07e1e..e2712c6 100644
--- a/frontend/src/styles/layout/structure.css
+++ b/frontend/src/styles/layout/structure.css
@@ -98,7 +98,7 @@
.main-content__actions-divider {
display: inline-flex;
align-items: center;
- color: var(--muted);
+ color: var(--muted-subtle);
}
.main-content__title {
diff --git a/frontend/src/styles/sidebar/sidebar.css b/frontend/src/styles/sidebar/sidebar.css
index 6a21907..4d38da3 100644
--- a/frontend/src/styles/sidebar/sidebar.css
+++ b/frontend/src/styles/sidebar/sidebar.css
@@ -229,14 +229,19 @@
border-radius: 0.5rem;
box-shadow: 0 12px 28px var(--shadow-strong);
min-width: 220px;
- z-index: 20;
+ z-index: 2500000;
overflow: hidden;
}
+.menu[data-floating-position] {
+ top: auto;
+ left: auto;
+ min-width: var(--floating-min-width, 220px);
+}
+
.menu__list {
- max-height: 260px;
overflow-y: auto;
- padding: 0.35rem 0.5rem;
+ padding: 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
@@ -265,7 +270,7 @@
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
- border-radius: 0.4rem;
+ border-radius: 0.25rem;
background: none;
color: var(--sidebar-fg);
font: inherit;
diff --git a/frontend/src/ui/BreadcrumbTrail.jsx b/frontend/src/ui/BreadcrumbTrail.jsx
index 3ea89da..a6fed37 100644
--- a/frontend/src/ui/BreadcrumbTrail.jsx
+++ b/frontend/src/ui/BreadcrumbTrail.jsx
@@ -24,7 +24,8 @@ const normalizeEntries = (entries) =>
const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null, raw: null };
const WIDTH_TOLERANCE = 1;
-const WIDTH_BUFFER_RATIO = 0.99;
+const WIDTH_BUFFER_RATIO = 0.95;
+const WIDTH_CHANGE_TOLERANCE = 0.25;
const BreadcrumbTrail = ({
entries = [],
@@ -43,6 +44,7 @@ const BreadcrumbTrail = ({
const ellipsisButtonRef = useRef(null);
const [availableWidth, setAvailableWidth] = useState(null);
const [startIndex, setStartIndex] = useState(0);
+ const measureRafRef = useRef(null);
const {
isOpen: ellipsisMenuOpen,
@@ -58,18 +60,13 @@ const BreadcrumbTrail = ({
});
useEffect(() => {
- setStartIndex(0);
closeEllipsisMenu();
}, [normalized, shouldTruncateFromStart, closeEllipsisMenu]);
useEffect(() => {
- const container = containerRef.current;
- if (!container || typeof ResizeObserver === 'undefined') {
- return undefined;
- }
-
- const updateWidth = () => {
- const host = containerRef.current;
+ const resolveHost = () => containerRef.current?.parentElement || containerRef.current;
+ const measure = () => {
+ const host = resolveHost();
if (!host) {
return;
}
@@ -77,14 +74,56 @@ const BreadcrumbTrail = ({
if (!nextWidth) {
return;
}
- setAvailableWidth((prev) => (prev && Math.abs(prev - nextWidth) < 0.5 ? prev : nextWidth));
+ setAvailableWidth((prev) => (
+ prev && Math.abs(prev - nextWidth) < WIDTH_CHANGE_TOLERANCE ? prev : nextWidth
+ ));
};
- updateWidth();
+ const scheduleMeasure = () => {
+ if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
+ measure();
+ return;
+ }
+ if (measureRafRef.current) {
+ cancelAnimationFrame(measureRafRef.current);
+ }
+ measureRafRef.current = window.requestAnimationFrame(() => {
+ measureRafRef.current = null;
+ measure();
+ });
+ };
- const observer = new ResizeObserver(updateWidth);
- observer.observe(container.parentElement || container);
- return () => observer.disconnect();
+ scheduleMeasure();
+
+ if (typeof ResizeObserver === 'undefined') {
+ return () => {
+ if (measureRafRef.current) {
+ cancelAnimationFrame(measureRafRef.current);
+ measureRafRef.current = null;
+ }
+ };
+ }
+
+ const host = resolveHost();
+ if (!host) {
+ return () => {
+ if (measureRafRef.current) {
+ cancelAnimationFrame(measureRafRef.current);
+ measureRafRef.current = null;
+ }
+ };
+ }
+
+ const observer = new ResizeObserver(scheduleMeasure);
+ observer.observe(host);
+
+ return () => {
+ observer.disconnect();
+ if (measureRafRef.current) {
+ cancelAnimationFrame(measureRafRef.current);
+ measureRafRef.current = null;
+ }
+ };
}, []);
useLayoutEffect(() => {
@@ -94,7 +133,8 @@ const BreadcrumbTrail = ({
const container = containerRef.current;
const measurement = measurementRef.current;
- if (!container || !measurement) {
+ const host = container?.parentElement || container;
+ if (!container || !measurement || !host) {
return;
}
@@ -142,7 +182,7 @@ const BreadcrumbTrail = ({
ellipsisNode.style.display = originalEllipsisDisplay ?? 'none';
}
- const available = availableWidth ?? container.getBoundingClientRect().width;
+ const available = availableWidth ?? host.getBoundingClientRect().width;
if (!available || !widths.length) {
return;
}
@@ -283,33 +323,43 @@ const BreadcrumbTrail = ({
className="breadcrumb-trail breadcrumb-trail--measure"
aria-hidden="true"
>
-
{ELLIPSIS.label}
-
- {measurementEntries.map((entry, index) => (
-
- {index > 0 ? (
-
+ {measurementEntries.map((entry, index) => {
+ const isLast = index === measurementEntries.length - 1;
+ const isInteractive = Boolean(entry.onClick) && !isLast;
+ const MeasurementTag = isInteractive ? 'button' : 'span';
+
+ return (
+
+ {index > 0 ? (
+
+ {separator}
+
+ ) : null}
+
- {separator}
-
- ) : null}
-
- {entry.label}
-
-
- ))}
+ {entry.label}
+
+
+ );
+ })}
{ellipsisMenuOpen
&& hasHiddenEntries
@@ -321,6 +371,7 @@ const BreadcrumbTrail = ({
role="menu"
ref={ellipsisMenuRef}
style={ellipsisMenuStyle}
+ data-floating-position
>
{hiddenEntries.map((hiddenEntry) => (
diff --git a/frontend/src/ui/QuickAddMenu.jsx b/frontend/src/ui/QuickAddMenu.jsx
index f239735..2def0e2 100644
--- a/frontend/src/ui/QuickAddMenu.jsx
+++ b/frontend/src/ui/QuickAddMenu.jsx
@@ -144,6 +144,23 @@ const QuickAddMenu = ({
);
const canCreate = Boolean(onCreate);
+ const isAnchoredMenu = positionStrategy === 'absolute' && align === 'start';
+ const menuClassName = 'menu menu--floating';
+ const anchoredMenuStyle = isAnchoredMenu && menuStyle
+ ? {
+ top: menuStyle.top,
+ left: menuStyle.left,
+ ...(menuStyle.width ? { width: menuStyle.width } : null),
+ }
+ : undefined;
+ const menuInlineStyle = isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined;
+ const hasFloatingWidthVar = Boolean(menuInlineStyle && Object.prototype.hasOwnProperty.call(menuInlineStyle, '--floating-min-width'));
+ const menuStyleWithVar = hasFloatingWidthVar
+ ? menuInlineStyle
+ : {
+ ...(menuInlineStyle || {}),
+ '--floating-min-width': `${Math.max(menuMinWidth, 0)}px`,
+ };
return (
@@ -162,10 +179,11 @@ const QuickAddMenu = ({
{isOpen ? (
{canCreate ? (
) : null}
-
+
{filteredOptions.length ? (
filteredOptions.map((option) => {
const key = option.id ?? option.index;
diff --git a/frontend/src/ui/useFloatingMenu.js b/frontend/src/ui/useFloatingMenu.js
index 3140ba6..e46e113 100644
--- a/frontend/src/ui/useFloatingMenu.js
+++ b/frontend/src/ui/useFloatingMenu.js
@@ -28,8 +28,10 @@ const formatStyle = (metrics) => {
position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed',
top: metrics.top,
left: metrics.left,
- minWidth: metrics.minWidth,
};
+ if (typeof metrics.minWidth === 'number') {
+ style['--floating-min-width'] = `${Math.max(metrics.minWidth, 0)}px`;
+ }
if (metrics.width) {
style.width = metrics.width;
}