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:
@@ -1,7 +1,9 @@
|
||||
import React, { useMemo, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { MemberWithUser } from '@backspace/shared';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useFloatingPosition } from '../../hooks/useFloatingPosition';
|
||||
|
||||
const MAX_RESULTS = 8;
|
||||
|
||||
@@ -9,13 +11,15 @@ interface MentionPopoverProps {
|
||||
query: string;
|
||||
selectedIndex: number;
|
||||
onSelect: (member: MemberWithUser) => void;
|
||||
anchorRef: React.RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export function MentionPopover({ query, selectedIndex, onSelect }: MentionPopoverProps) {
|
||||
export function MentionPopover({ query, selectedIndex, onSelect, anchorRef }: MentionPopoverProps) {
|
||||
const members = useSpaceStore((s) => s.members);
|
||||
const spaces = useSpaceStore((s) => s.spaces);
|
||||
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
const floatingRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const ownerId = spaces.find((s) => s.id === currentSpaceId)?.ownerId;
|
||||
|
||||
@@ -30,6 +34,12 @@ export function MentionPopover({ query, selectedIndex, onSelect }: MentionPopove
|
||||
.slice(0, MAX_RESULTS);
|
||||
}, [members, query]);
|
||||
|
||||
const { style } = useFloatingPosition(anchorRef, floatingRef, {
|
||||
placement: 'top',
|
||||
offset: 4,
|
||||
enabled: filtered.length > 0,
|
||||
});
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
selectedRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
@@ -46,8 +56,8 @@ export function MentionPopover({ query, selectedIndex, onSelect }: MentionPopove
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-full left-0 w-[280px] mb-1 z-50">
|
||||
return createPortal(
|
||||
<div ref={floatingRef} style={style} className="w-[280px]">
|
||||
<div className="bg-surface-elevated rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.15),0_8px_16px_rgba(0,0,0,0.24)] overflow-hidden max-h-[320px] overflow-y-auto scrollbar-thin">
|
||||
<div className="px-2 py-1.5 text-[11px] font-bold text-txt-tertiary uppercase tracking-wider">
|
||||
Members
|
||||
@@ -88,6 +98,7 @@ export function MentionPopover({ query, selectedIndex, onSelect }: MentionPopove
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
|
||||
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const inputContainerRef = useRef<HTMLDivElement>(null);
|
||||
const sendMessage = useChatStore((s) => s.sendMessage);
|
||||
const replyTo = useChatStore((s) => s.replyTo);
|
||||
const setReplyTo = useChatStore((s) => s.setReplyTo);
|
||||
@@ -249,6 +250,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={inputContainerRef}
|
||||
className={`relative bg-surface-input md:bg-transparent ${replyTo ? 'rounded-b-lg' : 'rounded-lg md:rounded-none'} overflow-visible`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
@@ -259,6 +261,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
|
||||
query={mentionState.query}
|
||||
selectedIndex={mentionState.selectedIndex}
|
||||
onSelect={selectMention}
|
||||
anchorRef={inputContainerRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user