mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 04:15:44 +00:00
Merge branch 'ui_ux' of github.com:Tria-plc/emaui into WorkflowChange
This commit is contained in:
@@ -157,8 +157,8 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
height: rem(40),
|
||||
borderRadius: rem(10),
|
||||
opacity: item.soon ? 0.55 : 1,
|
||||
color: branchActive ? 'var(--mantine-color-blue-6)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-color-blue-light)' : undefined,
|
||||
color: branchActive ? 'var(--mantine-primary-color-filled)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-primary-color-light)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ItemIcon size={20} stroke={1.6} />
|
||||
@@ -222,7 +222,10 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
opened={hasChildren ? opened : undefined}
|
||||
onChange={hasChildren ? onToggle : undefined}
|
||||
onClick={() => !hasChildren && onNavigate(item)}
|
||||
variant="light"
|
||||
// A leaf that is the current page is filled in the app's primary; a
|
||||
// branch whose child is current stays a tint, so the filled item is
|
||||
// always exactly the page you are on.
|
||||
variant={hasChildren ? 'light' : 'filled'}
|
||||
styles={{
|
||||
root: { borderRadius: rem(10), opacity: item.soon ? 0.7 : 1 },
|
||||
label: { fontWeight: 500 },
|
||||
|
||||
@@ -2,17 +2,54 @@ import { Group, Stack, Text, Title } from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/**
|
||||
* The page's name. A `ReactNode` rather than a string because detail pages
|
||||
* title themselves with the record they are showing, which is often a
|
||||
* localised or composed value rather than a literal.
|
||||
*/
|
||||
title: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
/**
|
||||
* Identifiers that belong beside the title rather than under it — a
|
||||
* reference number, a status badge. Rendered inline, small and dimmed.
|
||||
*/
|
||||
meta?: ReactNode;
|
||||
/** Right-aligned actions (e.g. a primary button). */
|
||||
action?: ReactNode;
|
||||
/**
|
||||
* Set when the header sits directly inside a gapped `<Stack>`, which already
|
||||
* spaces it from what follows — the built-in margin would double that gap.
|
||||
*/
|
||||
noMargin?: boolean;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, subtitle, action }: PageHeaderProps) {
|
||||
/**
|
||||
* The heading every backoffice page starts with.
|
||||
*
|
||||
* It exists because 25 pages each rolled their own: eight at `order={2}`,
|
||||
* seventeen at `order={3}`, with `mb="xs"`, `mb={4}`, `mb="lg"` and nothing at
|
||||
* all between them. Clicking between two screens moved the title, which is the
|
||||
* single loudest signal that a product was assembled rather than designed.
|
||||
*
|
||||
* `order={2}` is deliberate — it is the page's only `h2`, sitting under the
|
||||
* app-level `h1`, so the heading outline reads correctly for a screen reader.
|
||||
* Section headings inside cards stay at `order={4}`/`{5}` and are not this
|
||||
* component's business.
|
||||
*/
|
||||
export function PageHeader({ title, subtitle, meta, action, noMargin }: PageHeaderProps) {
|
||||
return (
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap" gap="sm">
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="flex-end"
|
||||
wrap="wrap"
|
||||
gap="sm"
|
||||
mb={noMargin ? undefined : 'lg'}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Title order={2}>{title}</Title>
|
||||
<Group gap="sm" align="center" wrap="wrap">
|
||||
<Title order={2}>{title}</Title>
|
||||
{meta}
|
||||
</Group>
|
||||
{subtitle && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{subtitle}
|
||||
|
||||
27
libs/ui/src/lib/layout/SkipLink.tsx
Normal file
27
libs/ui/src/lib/layout/SkipLink.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/** The id the skip link targets. Exported so the main region cannot drift. */
|
||||
export const MAIN_CONTENT_ID = 'ema-main-content';
|
||||
|
||||
/**
|
||||
* "Skip to main content" — the first thing a keyboard user should reach.
|
||||
*
|
||||
* Both apps put a sidebar of 20-plus navigation items before the page body, so
|
||||
* without this, reaching the actual content means tabbing through every one of
|
||||
* them on every navigation. WCAG 2.4.1 asks for a bypass; there was none.
|
||||
*
|
||||
* Hidden until focused, which is why it is positioned off-screen rather than
|
||||
* `display: none` — the latter would make it unfocusable and defeat the point.
|
||||
* Styling lives in `semantic.css` as `.ema-skip-link`.
|
||||
*
|
||||
* Render it as the first child of the shell, before the header.
|
||||
*/
|
||||
export function SkipLink() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a className="ema-skip-link" href={`#${MAIN_CONTENT_ID}`}>
|
||||
{t('a11y.skipToContent', 'Skip to main content')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user