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 ? (
@@ -183,7 +201,7 @@ const QuickAddMenu = ({
) : 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; }