feat: floating glass navigation and action bars in modals
Replace inline Save/Discard/Reset buttons with sticky glass-bubble pills that float at the bottom of scrollable modal content. Make SpaceSettings tab sidebar sticky with glass material. Convert RoleEditView "Back to roles" into a sticky glass pill at the top. UserSettings Log Out + Save always visible in a glass pill with separator. Also includes floating position hook and popover/tooltip improvements from prior work.
This commit is contained in:
@@ -49,6 +49,8 @@ export function ContextMenu({ items, children }: ContextMenuProps) {
|
||||
if (rect.bottom > window.innerHeight) {
|
||||
newPosition.y = window.innerHeight - rect.height - 8;
|
||||
}
|
||||
if (newPosition.x < 8) newPosition.x = 8;
|
||||
if (newPosition.y < 8) newPosition.y = 8;
|
||||
|
||||
if (newPosition.x !== position.x || newPosition.y !== position.y) {
|
||||
setPosition(newPosition);
|
||||
@@ -62,7 +64,7 @@ export function ContextMenu({ items, children }: ContextMenuProps) {
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-[200] min-w-[180px] py-1.5 bg-surface-elevated rounded-md shadow-elevation-high animate-fade-in"
|
||||
className="fixed z-[200] min-w-[180px] py-1.5 bg-surface-elevated rounded-md shadow-elevation-high animate-fade-in max-h-[calc(100vh-16px)] overflow-y-auto scrollbar-thin"
|
||||
style={{ left: position.x, top: position.y }}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
|
||||
@@ -30,9 +30,9 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md'
|
||||
className="absolute inset-0 bg-surface-overlay"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={`relative ${maxWidth} w-full mx-4 bg-surface-elevated rounded-lg shadow-xl animate-slide-up`}>
|
||||
<div className={`relative ${maxWidth} w-full mx-4 max-h-[calc(100vh-2rem)] flex flex-col bg-surface-elevated rounded-lg shadow-xl animate-slide-up`}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-4 pt-4">
|
||||
<div className="flex items-center justify-between px-4 pt-4 flex-shrink-0">
|
||||
<h2 className="text-xl font-bold text-txt-primary">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -44,7 +44,7 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md'
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<div className="p-4 overflow-y-auto scrollbar-thin flex-1 min-h-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useFloatingPosition } from '../../hooks/useFloatingPosition';
|
||||
|
||||
interface TooltipProps {
|
||||
content: string;
|
||||
@@ -10,6 +12,14 @@ interface TooltipProps {
|
||||
export function Tooltip({ content, children, position = 'right', delay = 200 }: TooltipProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const floatingRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { style } = useFloatingPosition(anchorRef, floatingRef, {
|
||||
placement: position,
|
||||
offset: 8,
|
||||
enabled: isVisible,
|
||||
});
|
||||
|
||||
const show = () => {
|
||||
timeoutRef.current = setTimeout(() => setIsVisible(true), delay);
|
||||
@@ -26,22 +36,18 @@ export function Tooltip({ content, children, position = 'right', delay = 200 }:
|
||||
};
|
||||
}, []);
|
||||
|
||||
const positionClasses: Record<string, string> = {
|
||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex" onMouseEnter={show} onMouseLeave={hide}>
|
||||
<div ref={anchorRef} className="relative inline-flex" onMouseEnter={show} onMouseLeave={hide}>
|
||||
{children}
|
||||
{isVisible && (
|
||||
{isVisible && createPortal(
|
||||
<div
|
||||
className={`absolute z-[200] px-3 py-1.5 text-sm font-medium text-txt-primary bg-surface-elevated rounded-md shadow-elevation-high whitespace-nowrap pointer-events-none ${positionClasses[position]}`}
|
||||
ref={floatingRef}
|
||||
style={style}
|
||||
className="px-3 py-1.5 text-sm font-medium text-txt-primary bg-surface-elevated rounded-md shadow-elevation-high whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user