mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
UI fixes
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
IconLogout,
|
||||
IconUserCircle,
|
||||
} from '@tabler/icons-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import { ColorSchemeToggle } from './ColorSchemeToggle';
|
||||
@@ -36,6 +37,17 @@ interface AppHeaderProps {
|
||||
supportedLanguages: readonly string[];
|
||||
onNotificationsClick?: () => void;
|
||||
notificationCount?: number;
|
||||
/**
|
||||
* Rendered at the far left. The sidebar layout carries the brand in the
|
||||
* sidebar itself; the top-bar layout has no sidebar, so it passes the brand
|
||||
* here rather than leaving the chrome unbranded.
|
||||
*/
|
||||
brand?: ReactNode;
|
||||
/**
|
||||
* Breakpoint from which the burger is hidden. The top-bar layout only needs
|
||||
* it on small screens, where the drawer replaces the nav strip.
|
||||
*/
|
||||
burgerHiddenFrom?: string;
|
||||
}
|
||||
|
||||
export function AppHeader({
|
||||
@@ -50,17 +62,21 @@ export function AppHeader({
|
||||
supportedLanguages,
|
||||
onNotificationsClick,
|
||||
notificationCount,
|
||||
brand,
|
||||
burgerHiddenFrom,
|
||||
}: AppHeaderProps) {
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
|
||||
|
||||
return (
|
||||
<Group h="100%" px="lg" justify="space-between" wrap="nowrap">
|
||||
<Group gap="md" wrap="nowrap">
|
||||
{brand}
|
||||
{/* Hamburger — styled like user-management Top.tsx */}
|
||||
{/* The Burger itself owns the click so the control is a real, keyboard
|
||||
reachable <button>; the Box is chrome only. It previously wrapped a
|
||||
no-op button, which no keyboard user could operate. */}
|
||||
<Box
|
||||
hiddenFrom={burgerHiddenFrom}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge, Group, Menu, UnstyledButton, rem } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
import { IconChevronDown } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { NavItem } from './AppSidebar';
|
||||
@@ -23,7 +24,9 @@ interface AppTopNavProps {
|
||||
* scrolling strip, so with twenty-odd of them most were off-screen and the
|
||||
* grouping that the sidebar already had was thrown away. Here each section
|
||||
* collapses to a single labelled dropdown, which fits and keeps the same
|
||||
* information architecture as the sidebar.
|
||||
* information architecture as the sidebar. Sections that still do not fit
|
||||
* scroll horizontally rather than dropping off the edge — under ~1100px the
|
||||
* last one or two were simply unreachable.
|
||||
*/
|
||||
export function AppTopNav({ navItems, activePath, onNavigate }: AppTopNavProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -36,6 +39,7 @@ export function AppTopNav({ navItems, activePath, onNavigate }: AppTopNavProps)
|
||||
wrap="nowrap"
|
||||
role="navigation"
|
||||
aria-label={t('nav.primary', 'Primary')}
|
||||
style={{ flex: 1, minWidth: 0, overflowX: 'auto', scrollbarWidth: 'none' }}
|
||||
>
|
||||
{sections.map((section, index) => {
|
||||
// An unlabelled leading block (Dashboard) is a plain link, not a menu.
|
||||
@@ -148,40 +152,52 @@ interface TopNavButtonProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function TopNavButton({
|
||||
label,
|
||||
active,
|
||||
badge,
|
||||
soon,
|
||||
withChevron,
|
||||
onClick,
|
||||
}: TopNavButtonProps) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: rem(6),
|
||||
padding: `0 ${rem(14)}`,
|
||||
height: '100%',
|
||||
borderBottom: '2px solid',
|
||||
borderBottomColor: active ? 'var(--mantine-color-blue-6)' : 'transparent',
|
||||
color: active ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-gray-6)',
|
||||
fontWeight: active ? 600 : 500,
|
||||
fontSize: rem(14),
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: soon ? 0.55 : 1,
|
||||
marginBottom: -1,
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{badge !== null && badge !== undefined && (
|
||||
<Badge size="xs" variant="filled" color="red" radius="sm">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
{withChevron && <IconChevronDown size={14} stroke={2} />}
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* `Menu.Target` positions its dropdown against the ref it passes to its child,
|
||||
* so a plain function component here left every section menu anchored at the
|
||||
* top-left of the viewport, covering the header instead of opening under the
|
||||
* button that was clicked. The rest props carry Menu's own click and aria
|
||||
* handling onto the real button.
|
||||
*/
|
||||
const TopNavButton = forwardRef<HTMLButtonElement, TopNavButtonProps>(
|
||||
function TopNavButton(
|
||||
{ label, active, badge, soon, withChevron, onClick, ...others },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
{...others}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: rem(6),
|
||||
padding: `0 ${rem(14)}`,
|
||||
height: '100%',
|
||||
flexShrink: 0,
|
||||
borderBottom: '2px solid',
|
||||
borderBottomColor: active
|
||||
? 'var(--mantine-color-blue-6)'
|
||||
: 'transparent',
|
||||
color: active
|
||||
? 'var(--mantine-color-blue-6)'
|
||||
: 'var(--mantine-color-gray-6)',
|
||||
fontWeight: active ? 600 : 500,
|
||||
fontSize: rem(14),
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: soon ? 0.55 : 1,
|
||||
marginBottom: -1,
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{badge !== null && badge !== undefined && (
|
||||
<Badge size="xs" variant="filled" color="red" radius="sm">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
{withChevron && <IconChevronDown size={14} stroke={2} />}
|
||||
</UnstyledButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user