feat: implement ModalFooter component for consistent modal action layout

This commit is contained in:
estifanos
2026-08-05 08:50:38 +00:00
parent c8df8e1578
commit bbae230ad5
21 changed files with 152 additions and 74 deletions

View File

@@ -31,6 +31,24 @@ export const emaTheme = createTheme({
md: '0 4px 20px rgba(15,23,42,0.08)',
lg: '0 8px 30px rgba(15,23,42,0.12)',
},
components: {
// Mantine's stock scroll wrapper (NativeScrollArea) discards the
// max-height it's handed unless scrollAreaComponent is set, so a modal
// taller than the viewport just gets clipped with no way to scroll it.
// Making the body the scrollport here fixes every Modal/Drawer at once.
Modal: {
styles: {
content: { display: 'flex', flexDirection: 'column', maxHeight: '90dvh' },
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
},
},
Drawer: {
styles: {
content: { display: 'flex', flexDirection: 'column' },
body: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto' },
},
},
},
other: {
heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
},

View File

@@ -1,5 +1,6 @@
export * from "./lib/input/BilingualInput";
export * from "./lib/feedback/ConfirmModal";
export * from "./lib/feedback/ModalFooter";
export * from "./lib/feedback/ApiErrorAlert";
export * from "./lib/feedback/notify";
export * from "./lib/feedback/FeatureUnavailable";

View File

@@ -1,4 +1,5 @@
import { Modal, Button, Group, Text } from '@mantine/core';
import { Modal, Button, Text } from '@mantine/core';
import { ModalFooter } from './ModalFooter';
interface ConfirmModalProps {
opened: boolean;
@@ -26,14 +27,14 @@ export function ConfirmModal({
<Text size="sm" mb="xl">
{message}
</Text>
<Group justify="flex-end">
<ModalFooter>
<Button variant="subtle" onClick={onClose} disabled={loading}>
{cancelLabel}
</Button>
<Button color="red" onClick={onConfirm} loading={loading}>
{confirmLabel}
</Button>
</Group>
</ModalFooter>
</Modal>
);
}

View File

@@ -0,0 +1,38 @@
import { Group, type GroupProps } from '@mantine/core';
import type { CSSProperties } from 'react';
/**
* Action row pinned to the bottom of a modal.
*
* Modal bodies scroll (see the Modal styles in the theme), which would carry the
* confirm button off-screen on a tall modal. Sticking to the bottom of the
* scrollport keeps the decision reachable from any scroll position. Negative
* margins bleed the background over the body's padding so nothing shows through
* underneath; on a modal short enough not to scroll this renders identically to
* a plain Group.
*/
export function ModalFooter({
children,
style,
...props
}: Omit<GroupProps, 'style'> & { style?: CSSProperties }) {
return (
<Group
justify="flex-end"
{...props}
style={{
position: 'sticky',
bottom: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
marginInline: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
marginBottom: 'calc(var(--mb-padding, var(--mantine-spacing-md)) * -1)',
padding: 'var(--mb-padding, var(--mantine-spacing-md))',
background: 'var(--mantine-color-body)',
borderTop: '1px solid var(--mantine-color-default-border)',
zIndex: 1,
...style,
}}
>
{children}
</Group>
);
}