mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 01:22:49 +00:00
feat: implement ModalFooter component for consistent modal action layout
This commit is contained in:
@@ -44,7 +44,7 @@ import {
|
||||
useUpdateLicenseTemplateMutation,
|
||||
type LicenseTemplate,
|
||||
} from '@ema-platform/api';
|
||||
import { EmptyState, ErrorState, PageHeader } from '@ema-platform/ui';
|
||||
import { EmptyState, ErrorState, ModalFooter, PageHeader } from '@ema-platform/ui';
|
||||
import { authStorage, usePermissions } from '@ema-platform/auth';
|
||||
import { PERMISSIONS } from '../../../layouts/nav-config';
|
||||
|
||||
@@ -529,7 +529,7 @@ export function CertificateDesignerPage() {
|
||||
'Starts from the live design, or the built-in layout if this type has none.',
|
||||
)}
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={() => setNewOpen(false)}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
@@ -550,7 +550,7 @@ export function CertificateDesignerPage() {
|
||||
>
|
||||
{t('designer.create', 'Create')}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Container>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCertificate } from '@tabler/icons-react';
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import {
|
||||
useGetCertificationsQuery,
|
||||
useCreateCertificationMutation,
|
||||
@@ -199,10 +199,10 @@ export function CertificationPage() {
|
||||
|
||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('certification.confirmDelete')} size="sm">
|
||||
<Text mb="md">{t('certification.deleteConfirmText', { name: deleteTarget?.name?.[locale] })}</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">{t('certification.cancel')}</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">{t('certification.delete')}</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
useErrorHandler,
|
||||
AdvancedTable,
|
||||
useServerTable,
|
||||
ModalFooter,
|
||||
type AdvancedColumn,
|
||||
} from "@ema-platform/ui";
|
||||
import { LocationPage } from "../../location/pages/LocationPage";
|
||||
@@ -388,14 +389,14 @@ function ProfessionTab() {
|
||||
name: deleteTarget?.name?.[locale] ?? "",
|
||||
})}
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">
|
||||
{t("configuration.cancel")}
|
||||
</Button>
|
||||
<Button color="red" onClick={confirmDeleteProf} size="sm">
|
||||
{t("configuration.delete")}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
IconX,
|
||||
IconDetails
|
||||
} from "@tabler/icons-react";
|
||||
import { notify, useErrorHandler } from "@ema-platform/ui";
|
||||
import { notify, useErrorHandler, ModalFooter } from "@ema-platform/ui";
|
||||
import {
|
||||
useGetExamQuery,
|
||||
useUpdateExamMutation,
|
||||
@@ -494,14 +494,14 @@ export function ExamDetailPage() {
|
||||
mode="manual"
|
||||
actions={setWhatAction}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeAssign} size="sm">
|
||||
{t("exam.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -531,14 +531,14 @@ export function ExamDetailPage() {
|
||||
onChange={setDraftQuestions}
|
||||
mode="random"
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeAssign} size="sm">
|
||||
{t("exam.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleAssign} size="sm" loading={isAssigning}>
|
||||
{t("exam.saveAssignments")}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
IconClipboardList,
|
||||
IconDetails,
|
||||
} from "@tabler/icons-react";
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from "@ema-platform/ui";
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from "@ema-platform/ui";
|
||||
import { useGetCertificationsQuery } from "../../certification/api/certification-api";
|
||||
import {
|
||||
useGetExamsQuery,
|
||||
@@ -600,14 +600,14 @@ export function ExamPage() {
|
||||
name: deleteTarget?.title?.[locale] ?? "",
|
||||
})}
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">
|
||||
{t("exam.cancel")}
|
||||
</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">
|
||||
{t("exam.delete")}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ModalFooter } from '@ema-platform/ui';
|
||||
import type { ResolvedAction } from '../config/actions';
|
||||
|
||||
/** Reason codes offered per action. Free text is always available too. */
|
||||
@@ -290,7 +290,7 @@ export function DecisionConfirmModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={onClose}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
@@ -310,7 +310,7 @@ export function DecisionConfirmModal({
|
||||
>
|
||||
{t(action.labelKey)}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
useScheduleInspectionMutation,
|
||||
type RemarkTargetType,
|
||||
} from '@ema-platform/api';
|
||||
import { ErrorState } from '@ema-platform/ui';
|
||||
import { ErrorState, ModalFooter } from '@ema-platform/ui';
|
||||
import { usePermissions } from '@ema-platform/auth';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { DecisionBar } from '../components/DecisionBar';
|
||||
@@ -894,7 +894,7 @@ export function LicenseReviewPage() {
|
||||
value={inspectionDate}
|
||||
onChange={(e) => setInspectionDate(e.currentTarget.value)}
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Tooltip
|
||||
label={t('review.pickDate', 'Pick a date and time first')}
|
||||
disabled={Boolean(inspectionDate)}
|
||||
@@ -924,7 +924,7 @@ export function LicenseReviewPage() {
|
||||
>
|
||||
<IconCheck size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -942,7 +942,7 @@ export function LicenseReviewPage() {
|
||||
autosize
|
||||
minRows={3}
|
||||
/>
|
||||
<Group grow>
|
||||
<ModalFooter grow>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="teal"
|
||||
@@ -989,7 +989,7 @@ export function LicenseReviewPage() {
|
||||
>
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Container>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconSettings, IconMap, IconInfoCircle, IconPlus } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||
import { LocationTree } from '../components/LocationTree';
|
||||
import { LocationDetail } from '../components/LocationDetail';
|
||||
import { LocationForm } from '../components/LocationForm';
|
||||
@@ -226,14 +226,14 @@ export function LocationPage() {
|
||||
name: selectedLocation?.names[locale] ?? '',
|
||||
})}
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDeleteModal} size="sm">
|
||||
{t('location.cancel')}
|
||||
</Button>
|
||||
<Button color="red" onClick={handleDeleteConfirm} size="sm">
|
||||
{t('location.delete')}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<LocationTypeModal opened={typeModalOpened} onClose={closeTypeModal} />
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
IconInfoCircle,
|
||||
IconLock,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||
import {
|
||||
extractErrorMessage,
|
||||
localized,
|
||||
@@ -364,14 +364,14 @@ function FeeEditModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end" mt="xs">
|
||||
<ModalFooter mt="xs">
|
||||
<Button variant="default" onClick={onClose} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={save} loading={isLoading}>
|
||||
Save fees
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconEdit, IconTrash, IconPlus, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
|
||||
import {
|
||||
useGetQuestionsQuery,
|
||||
@@ -255,10 +255,10 @@ export function QuestionPage() {
|
||||
|
||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('question.confirmDelete')} size="sm">
|
||||
<Text mb="md">{t('question.deleteConfirmText')}</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">{t('question.cancel')}</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">{t('question.delete')}</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Alert,
|
||||
} from '@mantine/core';
|
||||
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||
import { useApiQuery } from '@ema-platform/api';
|
||||
import { useCreateResultMutation, useUpdateResultMutation } from '../api/result-api';
|
||||
import type { Exam } from '../../exam/types/exam';
|
||||
@@ -195,12 +195,12 @@ export function RecordResultModal({
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={onClose} size="sm">{t('result.cancel')}</Button>
|
||||
<Button onClick={handleSave} size="sm" loading={isSaving}>
|
||||
{t('result.saveResult')}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
IconChartBar,
|
||||
IconSearch,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import type { BilingualValue } from '@ema-platform/ui';
|
||||
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation, useUpdateResultMutation } from '../api/result-api';
|
||||
import { useGetExamsQuery } from '../../exam/api/exam-api';
|
||||
@@ -462,7 +462,7 @@ export function ResultPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={handleDetailClose} size="sm">{t('result.close')}</Button>
|
||||
<Button
|
||||
onClick={handleDetailSave}
|
||||
@@ -472,7 +472,7 @@ export function ResultPage() {
|
||||
>
|
||||
{t('result.save')}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed" ta="center" py="xl">{t('result.noData')}</Text>
|
||||
@@ -481,10 +481,10 @@ export function ResultPage() {
|
||||
|
||||
<Modal opened={deleteOpened} onClose={closeDelete} title={t('result.confirmDelete')} size="sm">
|
||||
<Text mb="md">{t('result.deleteConfirmText')}</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closeDelete} size="sm">{t('result.cancel')}</Button>
|
||||
<Button color="red" onClick={handleDelete} size="sm">{t('result.delete')}</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
{/* Choose exam, then record */}
|
||||
@@ -501,10 +501,10 @@ export function ResultPage() {
|
||||
searchable
|
||||
required
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={closePicker} size="sm">{t('result.cancel')}</Button>
|
||||
<Button onClick={startRecord} size="sm" disabled={!pickerExamId}>{t('result.continue')}</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
IconSettings,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -495,12 +495,12 @@ export function VesselRegistrationFormBuilderPage() {
|
||||
onChange={(e) => setDraftRequired(e.currentTarget.checked)}
|
||||
/>
|
||||
<Divider />
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={() => setDrawerOpen(false)}>Cancel</Button>
|
||||
<Button color="teal" onClick={saveField}>
|
||||
{isNew ? 'Add Field' : 'Save Changes'}
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
|
||||
@@ -516,10 +516,10 @@ export function VesselRegistrationFormBuilderPage() {
|
||||
Are you sure you want to remove <strong>{deleteTarget?.label}</strong> from the form?
|
||||
This cannot be undone.
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={() => setDeleteTarget(null)}>Cancel</Button>
|
||||
<Button color="red" onClick={confirmDelete}>Delete Field</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
type FieldErrors,
|
||||
type ValidationIssue,
|
||||
} from '@ema-platform/api';
|
||||
import { ModalFooter } from '@ema-platform/ui';
|
||||
import { ConfigDrivenSection } from '../components/ConfigDrivenSection';
|
||||
import { DocumentSlots } from '../components/DocumentSlots';
|
||||
import { StaffEvidence } from '../components/StaffEvidence';
|
||||
@@ -617,17 +618,19 @@ export function LicenseApplicationPage() {
|
||||
onChange={(v) => setNewStaff({ ...newStaff, yearsOfExperience: Number(v) || 0 })}
|
||||
min={0}
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!newStaff.fullName.trim() || !staffModal) return;
|
||||
await addStaff({ id: appId, roleKey: staffModal, ...newStaff });
|
||||
setNewStaff({ fullName: '', position: '', yearsOfExperience: 0 });
|
||||
setStaffModal(null);
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!newStaff.fullName.trim() || !staffModal) return;
|
||||
await addStaff({ id: appId, roleKey: staffModal, ...newStaff });
|
||||
setNewStaff({ fullName: '', position: '', yearsOfExperience: 0 });
|
||||
setStaffModal(null);
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Container>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
useGetMyOperatorTypesQuery,
|
||||
useUpdateMyOperatorTypesMutation,
|
||||
} from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, ModalFooter } from '@ema-platform/ui';
|
||||
|
||||
/**
|
||||
* The applicant's modes of operation — what they do, and therefore which
|
||||
@@ -199,7 +199,7 @@ export function OperationsFormContent({
|
||||
Applications already filed carry on as they are, and licences
|
||||
already issued to you stay valid and can still be renewed.
|
||||
</Text>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<ModalFooter gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setConfirmingRemoval(false)}
|
||||
@@ -209,7 +209,7 @@ export function OperationsFormContent({
|
||||
<Button color="orange" loading={saving} onClick={persist}>
|
||||
Remove and save
|
||||
</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { CountrySelect, notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { CountrySelect, notify, useErrorHandler, ModalFooter } from '@ema-platform/ui';
|
||||
import type { Seafarer } from './SeafarerRegistryPage';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -479,10 +479,10 @@ function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () =>
|
||||
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
||||
</SimpleGrid>
|
||||
<Select label="Status" data={['Approved', 'Pending', 'Expired']} defaultValue="Pending" />
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<ModalFooter mt="sm">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={() => { notify.success('Training record added (demo).'); onClose(); }}>Save</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
@@ -503,10 +503,10 @@ function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () =>
|
||||
</SimpleGrid>
|
||||
<Select label="Result" data={['Fit', 'Unfit', 'Conditional']} defaultValue="Fit" />
|
||||
<Textarea label="Remarks" placeholder="Any notes" autosize minRows={2} />
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<ModalFooter mt="sm">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={() => { notify.success('Medical record added (demo).'); onClose(); }}>Save</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
@@ -533,10 +533,10 @@ function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: ()
|
||||
<AmharicDatePicker label="To" value={toDate} onChange={setToDate} />
|
||||
</SimpleGrid>
|
||||
<TextInput label="Engagement Port" placeholder="Port name" />
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<ModalFooter mt="sm">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={() => { notify.success('Sea service record added (demo).'); onClose(); }}>Save</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
@@ -560,10 +560,10 @@ function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => voi
|
||||
<AmharicDatePicker label="Expiry Date" value={expiryDate} onChange={setExpiryDate} />
|
||||
</SimpleGrid>
|
||||
<Select label="Status" data={['Valid', 'Expired', 'Pending']} defaultValue="Valid" />
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<ModalFooter mt="sm">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={() => { notify.success('Certification added (demo).'); onClose(); }}>Save</Button>
|
||||
</Group>
|
||||
</ModalFooter>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -93,6 +93,22 @@ export const portalTheme = createTheme({
|
||||
Textarea: { defaultProps: { radius: 'md' } },
|
||||
Select: { defaultProps: { radius: 'md' } },
|
||||
PasswordInput: { defaultProps: { radius: 'md' } },
|
||||
// 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%)',
|
||||
|
||||
@@ -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%)',
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
38
libs/ui/src/lib/feedback/ModalFooter.tsx
Normal file
38
libs/ui/src/lib/feedback/ModalFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user