This commit is contained in:
2025-11-10 21:27:04 +01:00
parent ab81e76206
commit 8820ff1c15
11 changed files with 155 additions and 57 deletions
@@ -181,6 +181,7 @@ const SelectionAssignmentMenu = ({
ref={menuRef} ref={menuRef}
style={menuStyle || undefined} style={menuStyle || undefined}
role="menu" role="menu"
data-floating-position
> >
<div className="selection-assignment__header"> <div className="selection-assignment__header">
<input <input
@@ -111,6 +111,7 @@ const CapabilityDropdown = ({
role="menu" role="menu"
ref={menuRef} ref={menuRef}
className="menu menu--floating capability-dropdown__menu" className="menu menu--floating capability-dropdown__menu"
data-floating-position
> >
{total ? ( {total ? (
options.map((option) => { options.map((option) => {
+3
View File
@@ -14,6 +14,7 @@
--surface-subtle: oklch(0.96 0.002 calc(var(--neutral-hue) + var(--surface-hue-shift))); --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))); --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: 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))); --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))); --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))); --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)); --fg: oklch(0.89 0.015 var(--dark-foreground-hue));
--muted: oklch(0.72 0.02 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)); --sidebar-fg: oklch(0.78 0.02 var(--dark-foreground-hue));
--border: oklch(0.33 0.01 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))); --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)); --fg: oklch(0.89 0.015 var(--dark-foreground-hue));
--muted: oklch(0.72 0.02 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)); --sidebar-fg: oklch(0.78 0.02 var(--dark-foreground-hue));
--border: oklch(0.33 0.01 var(--dark-foreground-hue)); --border: oklch(0.33 0.01 var(--dark-foreground-hue));
+4 -6
View File
@@ -80,6 +80,8 @@
} }
.quick-add { .quick-add {
--quick-add-menu-offset: 0.35rem;
--quick-add-menu-min-width: 220px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
position: relative; position: relative;
@@ -128,10 +130,6 @@
gap: 0.25rem; gap: 0.25rem;
} }
.quick-add__menu {
padding: 0.25rem 0;
}
.quick-add__form { .quick-add__form {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -196,8 +194,8 @@
.selection-assignment__header input:focus-visible { .selection-assignment__header input:focus-visible {
outline: none; outline: none;
border-color: var(--selection-border); border-color: color-mix(in oklch, var(--accent) 60%, transparent);
box-shadow: 0 0 0 2px color-mix(in oklch, var(--selection) 25%, transparent); box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent) 35%, transparent);
} }
.selection-assignment__list { .selection-assignment__list {
+4 -1
View File
@@ -44,6 +44,10 @@
position: relative; position: relative;
} }
.documents-sort__quickmenu {
--quick-add-menu-min-width: 200px;
}
.documents-sort__trigger { .documents-sort__trigger {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -105,4 +109,3 @@
width: 1.1rem; width: 1.1rem;
height: 1.1rem; height: 1.1rem;
} }
@@ -84,19 +84,36 @@
cursor: default; 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) { .breadcrumb-trail__link:not(.is-current) {
cursor: pointer; cursor: pointer;
color: var(--accent); color: var(--accent);
} }
.breadcrumb-trail__link:not(.is-current):hover, .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; text-decoration: underline;
background: var(--accent-soft);
color: var(--accent);
} }
.breadcrumb-trail__separator { .breadcrumb-trail__separator {
color: var(--muted); color: var(--muted-subtle);
margin: 0 0.2rem; margin: 0;
} }
.breadcrumb-trail__ellipsis { .breadcrumb-trail__ellipsis {
@@ -130,4 +147,3 @@
.panel-section__body.scrollable { .panel-section__body.scrollable {
overflow-y: auto; overflow-y: auto;
} }
+1 -1
View File
@@ -98,7 +98,7 @@
.main-content__actions-divider { .main-content__actions-divider {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: var(--muted); color: var(--muted-subtle);
} }
.main-content__title { .main-content__title {
+9 -4
View File
@@ -229,14 +229,19 @@
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 12px 28px var(--shadow-strong); box-shadow: 0 12px 28px var(--shadow-strong);
min-width: 220px; min-width: 220px;
z-index: 20; z-index: 2500000;
overflow: hidden; overflow: hidden;
} }
.menu[data-floating-position] {
top: auto;
left: auto;
min-width: var(--floating-min-width, 220px);
}
.menu__list { .menu__list {
max-height: 260px;
overflow-y: auto; overflow-y: auto;
padding: 0.35rem 0.5rem; padding: 0.25rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.25rem;
@@ -265,7 +270,7 @@
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border: none; border: none;
border-radius: 0.4rem; border-radius: 0.25rem;
background: none; background: none;
color: var(--sidebar-fg); color: var(--sidebar-fg);
font: inherit; font: inherit;
+73 -22
View File
@@ -24,7 +24,8 @@ const normalizeEntries = (entries) =>
const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null, raw: null }; const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null, raw: null };
const WIDTH_TOLERANCE = 1; const WIDTH_TOLERANCE = 1;
const WIDTH_BUFFER_RATIO = 0.99; const WIDTH_BUFFER_RATIO = 0.95;
const WIDTH_CHANGE_TOLERANCE = 0.25;
const BreadcrumbTrail = ({ const BreadcrumbTrail = ({
entries = [], entries = [],
@@ -43,6 +44,7 @@ const BreadcrumbTrail = ({
const ellipsisButtonRef = useRef(null); const ellipsisButtonRef = useRef(null);
const [availableWidth, setAvailableWidth] = useState(null); const [availableWidth, setAvailableWidth] = useState(null);
const [startIndex, setStartIndex] = useState(0); const [startIndex, setStartIndex] = useState(0);
const measureRafRef = useRef(null);
const { const {
isOpen: ellipsisMenuOpen, isOpen: ellipsisMenuOpen,
@@ -58,18 +60,13 @@ const BreadcrumbTrail = ({
}); });
useEffect(() => { useEffect(() => {
setStartIndex(0);
closeEllipsisMenu(); closeEllipsisMenu();
}, [normalized, shouldTruncateFromStart, closeEllipsisMenu]); }, [normalized, shouldTruncateFromStart, closeEllipsisMenu]);
useEffect(() => { useEffect(() => {
const container = containerRef.current; const resolveHost = () => containerRef.current?.parentElement || containerRef.current;
if (!container || typeof ResizeObserver === 'undefined') { const measure = () => {
return undefined; const host = resolveHost();
}
const updateWidth = () => {
const host = containerRef.current;
if (!host) { if (!host) {
return; return;
} }
@@ -77,14 +74,56 @@ const BreadcrumbTrail = ({
if (!nextWidth) { if (!nextWidth) {
return; 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); scheduleMeasure();
observer.observe(container.parentElement || container);
return () => observer.disconnect(); 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(() => { useLayoutEffect(() => {
@@ -94,7 +133,8 @@ const BreadcrumbTrail = ({
const container = containerRef.current; const container = containerRef.current;
const measurement = measurementRef.current; const measurement = measurementRef.current;
if (!container || !measurement) { const host = container?.parentElement || container;
if (!container || !measurement || !host) {
return; return;
} }
@@ -142,7 +182,7 @@ const BreadcrumbTrail = ({
ellipsisNode.style.display = originalEllipsisDisplay ?? 'none'; ellipsisNode.style.display = originalEllipsisDisplay ?? 'none';
} }
const available = availableWidth ?? container.getBoundingClientRect().width; const available = availableWidth ?? host.getBoundingClientRect().width;
if (!available || !widths.length) { if (!available || !widths.length) {
return; return;
} }
@@ -283,14 +323,21 @@ const BreadcrumbTrail = ({
className="breadcrumb-trail breadcrumb-trail--measure" className="breadcrumb-trail breadcrumb-trail--measure"
aria-hidden="true" aria-hidden="true"
> >
<span <button
type="button"
className="breadcrumb-trail__link breadcrumb-trail__ellipsis-button" className="breadcrumb-trail__link breadcrumb-trail__ellipsis-button"
data-item-type="ellipsis" data-item-type="ellipsis"
style={{ display: 'none' }} style={{ display: 'none' }}
tabIndex={-1}
> >
{ELLIPSIS.label} {ELLIPSIS.label}
</span> </button>
{measurementEntries.map((entry, index) => ( {measurementEntries.map((entry, index) => {
const isLast = index === measurementEntries.length - 1;
const isInteractive = Boolean(entry.onClick) && !isLast;
const MeasurementTag = isInteractive ? 'button' : 'span';
return (
<React.Fragment key={`measure-${entry.id || index}`}> <React.Fragment key={`measure-${entry.id || index}`}>
{index > 0 ? ( {index > 0 ? (
<span <span
@@ -301,15 +348,18 @@ const BreadcrumbTrail = ({
{separator} {separator}
</span> </span>
) : null} ) : null}
<span <MeasurementTag
type={isInteractive ? 'button' : undefined}
className="breadcrumb-trail__link" className="breadcrumb-trail__link"
data-item-type="entry" data-item-type="entry"
data-entry-index={index} data-entry-index={index}
tabIndex={-1}
> >
{entry.label} {entry.label}
</span> </MeasurementTag>
</React.Fragment> </React.Fragment>
))} );
})}
</span> </span>
{ellipsisMenuOpen {ellipsisMenuOpen
&& hasHiddenEntries && hasHiddenEntries
@@ -321,6 +371,7 @@ const BreadcrumbTrail = ({
role="menu" role="menu"
ref={ellipsisMenuRef} ref={ellipsisMenuRef}
style={ellipsisMenuStyle} style={ellipsisMenuStyle}
data-floating-position
> >
<div className="menu__list"> <div className="menu__list">
{hiddenEntries.map((hiddenEntry) => ( {hiddenEntries.map((hiddenEntry) => (
+21 -3
View File
@@ -144,6 +144,23 @@ const QuickAddMenu = ({
); );
const canCreate = Boolean(onCreate); 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 ( return (
<div className={className ? `quick-add ${className}` : 'quick-add'}> <div className={className ? `quick-add ${className}` : 'quick-add'}>
@@ -162,10 +179,11 @@ const QuickAddMenu = ({
</button> </button>
{isOpen ? ( {isOpen ? (
<div <div
className="menu menu--floating quick-add__menu" className={menuClassName}
ref={menuRef} ref={menuRef}
style={menuStyle || undefined} style={menuStyleWithVar}
role="menu" role="menu"
data-floating-position
> >
{canCreate ? ( {canCreate ? (
<form className="quick-add__form" onSubmit={handleCreate}> <form className="quick-add__form" onSubmit={handleCreate}>
@@ -183,7 +201,7 @@ const QuickAddMenu = ({
</button> </button>
</form> </form>
) : null} ) : null}
<div className="menu__list quick-add__list" role="presentation"> <div className="menu__list" role="presentation">
{filteredOptions.length ? ( {filteredOptions.length ? (
filteredOptions.map((option) => { filteredOptions.map((option) => {
const key = option.id ?? option.index; const key = option.id ?? option.index;
+3 -1
View File
@@ -28,8 +28,10 @@ const formatStyle = (metrics) => {
position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed', position: metrics.strategy === 'absolute' ? 'absolute' : 'fixed',
top: metrics.top, top: metrics.top,
left: metrics.left, left: metrics.left,
minWidth: metrics.minWidth,
}; };
if (typeof metrics.minWidth === 'number') {
style['--floating-min-width'] = `${Math.max(metrics.minWidth, 0)}px`;
}
if (metrics.width) { if (metrics.width) {
style.width = metrics.width; style.width = metrics.width;
} }