feat(client): add Card header/footer, DescriptionList, and portal ContextMenu
- add Card header prop (primary/secondary variants) and footer prop (sticks to bottom) - add Breadcrumbs to all 5 pages; remove duplicate h1 headings - add pageToolbar layout for breadcrumbs + danger button aligned right - add DescriptionList component replacing raw dl/dt/dd markup; supports truncate+tooltip - rewrite ContextMenu to render dropdown via React portal to avoid clip/overflow issues - update EnvironmentCard to use Card header (primary), footer (Timestamp), and truncated DescriptionList - fix cog button contrast, square sizing, and right-edge alignment on colored header - add border to Timestamp pill for dark-mode visibility - add id badge pill styling to environment card header
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styles from './ContextMenu.module.css';
|
||||
|
||||
export interface ContextMenuItem {
|
||||
@@ -17,12 +18,20 @@ export interface ContextMenuProps {
|
||||
|
||||
export function ContextMenu({ items, trigger, align = 'right' }: ContextMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [coords, setCoords] = useState({ top: 0, left: 0, right: 0 });
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleOutside(e: MouseEvent) {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
||||
const target = e.target as Node;
|
||||
if (
|
||||
triggerRef.current &&
|
||||
!triggerRef.current.contains(target) &&
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(target)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
@@ -39,44 +48,64 @@ export function ContextMenu({ items, trigger, align = 'right' }: ContextMenuProp
|
||||
return () => document.removeEventListener('keydown', handleKey);
|
||||
}, [open]);
|
||||
|
||||
function openMenu() {
|
||||
if (!triggerRef.current) return;
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
setCoords({
|
||||
top: rect.bottom + window.scrollY + 4,
|
||||
left: rect.left + window.scrollX,
|
||||
right: window.innerWidth - rect.right - window.scrollX,
|
||||
});
|
||||
setOpen((v) => !v);
|
||||
}
|
||||
|
||||
const menuStyle =
|
||||
align === 'right'
|
||||
? { top: coords.top, right: coords.right }
|
||||
: { top: coords.top, left: coords.left };
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
<div className={styles.root} ref={triggerRef}>
|
||||
<div
|
||||
className={styles.triggerWrap}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen((v) => !v);
|
||||
openMenu();
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
{open && (
|
||||
<ul
|
||||
className={`${styles.menu} ${align === 'left' ? styles.alignLeft : styles.alignRight}`}
|
||||
role="menu"
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<li key={i} role="none">
|
||||
<button
|
||||
role="menuitem"
|
||||
className={`${styles.item} ${item.variant === 'danger' ? styles.danger : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className={styles.icon} aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{open &&
|
||||
createPortal(
|
||||
<ul
|
||||
ref={menuRef}
|
||||
className={styles.menu}
|
||||
style={{ ...menuStyle, position: 'fixed' }}
|
||||
role="menu"
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<li key={i} role="none">
|
||||
<button
|
||||
role="menuitem"
|
||||
className={`${styles.item} ${item.variant === 'danger' ? styles.danger : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className={styles.icon} aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user