mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: implement PageLoader component and register maritime loader as the default theme loader
This commit is contained in:
@@ -6,9 +6,76 @@
|
|||||||
<link rel="icon" type="image/png" href="/ema-logo.png" />
|
<link rel="icon" type="image/png" href="/ema-logo.png" />
|
||||||
<link rel="apple-touch-icon" href="/ema-logo.png" />
|
<link rel="apple-touch-icon" href="/ema-logo.png" />
|
||||||
<title>EMA Backoffice</title>
|
<title>EMA Backoffice</title>
|
||||||
|
<script>
|
||||||
|
// Apply the saved Mantine color scheme before paint to avoid a flash.
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('mantine-color-scheme-value') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-mantine-color-scheme', s);
|
||||||
|
} catch (e) {}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Boot splash — shown until React mounts into #root. Colors are
|
||||||
|
hardcoded (Mantine's default light/dark-7 body background) so the
|
||||||
|
splash never depends on the app's own stylesheet finishing its load. */
|
||||||
|
#ema-boot-splash {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
html[data-mantine-color-scheme='dark'] #ema-boot-splash {
|
||||||
|
background: #242424;
|
||||||
|
}
|
||||||
|
#ema-boot-splash svg {
|
||||||
|
width: 140px;
|
||||||
|
height: auto;
|
||||||
|
color: #2563eb;
|
||||||
|
animation: ema-boot-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ema-boot-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.55; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#ema-boot-splash svg { animation: none; }
|
||||||
|
}
|
||||||
|
/* React has committed into #root once it has a child; hide the splash. */
|
||||||
|
#root:not(:empty) ~ #ema-boot-splash {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
Static boot splash. Kept intentionally minimal (hull, funnel, mast,
|
||||||
|
flag — no cargo containers, no animated water) since this markup can't
|
||||||
|
import React. Source of truth for the full animated version is
|
||||||
|
libs/ui/src/lib/components/MaritimeLoader.tsx.
|
||||||
|
-->
|
||||||
|
<div id="ema-boot-splash" role="status" aria-live="polite" aria-label="Loading">
|
||||||
|
<svg viewBox="0 0 260 138" aria-hidden="true" focusable="false">
|
||||||
|
<ellipse cx="132" cy="113" rx="78" ry="7" fill="currentColor" opacity="0.12" />
|
||||||
|
<path d="M42 72H214l-4 6H48z" fill="currentColor" opacity="0.82" />
|
||||||
|
<path d="M166 25h10l3 17h-15z" fill="#eef3f5" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||||
|
<path d="M166.9 29h9.9l.6 3.5h-11.1z" fill="#078930" />
|
||||||
|
<path d="M166.2 32.5h11.2l.6 3.5h-12.4z" fill="#fcd116" />
|
||||||
|
<path d="M165.6 36h12.4l.6 3.5h-13.6z" fill="#da121a" />
|
||||||
|
<path d="M194 31V12M188 20h12M194 13l11 8M194 13l-9 8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="194" cy="11" r="2" fill="currentColor" />
|
||||||
|
<path d="M184 20V8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||||
|
<path d="M184 8c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#078930" />
|
||||||
|
<path d="M184 12c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#fcd116" />
|
||||||
|
<path d="M184 16c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#da121a" />
|
||||||
|
<path d="M28 76h205l-15 18c-8 10-20 15-33 15H66c-13 0-24-5-31-15L23 80c-2-2 0-4 5-4z" fill="#f8fbfc" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" />
|
||||||
|
<path d="M34 88h190l-6 6c-8 10-20 15-33 15H66c-13 0-24-5-31-15z" fill="currentColor" opacity="0.92" />
|
||||||
|
<path d="M36 87h188" fill="none" stroke="rgb(255 255 255 / 52%)" stroke-width="2.4" stroke-linecap="round" />
|
||||||
|
<path d="M206 81l15 1-8 9" fill="none" stroke="rgb(255 255 255 / 46%)" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ function SeafarerDetailDrawer({
|
|||||||
|
|
||||||
<Tabs.Panel value="sea-service" pt="sm">
|
<Tabs.Panel value="sea-service" pt="sm">
|
||||||
{loadingSea ? (
|
{loadingSea ? (
|
||||||
<Loader size="sm" />
|
<Loader size="sm" type="oval" />
|
||||||
) : (seaService ?? []).length === 0 ? (
|
) : (seaService ?? []).length === 0 ? (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t('seafarerRegistry.drawer.noSeaService', 'No sea-service records.')}
|
{t('seafarerRegistry.drawer.noSeaService', 'No sea-service records.')}
|
||||||
@@ -184,7 +184,7 @@ function SeafarerDetailDrawer({
|
|||||||
|
|
||||||
<Tabs.Panel value="medical" pt="sm">
|
<Tabs.Panel value="medical" pt="sm">
|
||||||
{loadingMedical ? (
|
{loadingMedical ? (
|
||||||
<Loader size="sm" />
|
<Loader size="sm" type="oval" />
|
||||||
) : (medical ?? []).length === 0 ? (
|
) : (medical ?? []).length === 0 ? (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t('seafarerRegistry.drawer.noMedical', 'No medical certificates.')}
|
{t('seafarerRegistry.drawer.noMedical', 'No medical certificates.')}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function VesselDetailDrawer({
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{loadingIncidents ? (
|
{loadingIncidents ? (
|
||||||
<Loader size="sm" />
|
<Loader size="sm" type="oval" />
|
||||||
) : (incidents ?? []).length === 0 ? (
|
) : (incidents ?? []).length === 0 ? (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
No incidents recorded.
|
No incidents recorded.
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider, mergeThemeOverrides } from '@mantine/core';
|
||||||
import { Notifications } from '@mantine/notifications';
|
import { Notifications } from '@mantine/notifications';
|
||||||
import { emaTheme } from '@ema-platform/shared';
|
import { emaTheme } from '@ema-platform/shared';
|
||||||
|
import { maritimeLoaderTheme } from '@ema-platform/ui';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const theme = mergeThemeOverrides(emaTheme, maritimeLoaderTheme);
|
||||||
|
|
||||||
export function MantineThemeProvider({ children }: { children: ReactNode }) {
|
export function MantineThemeProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<MantineProvider theme={emaTheme} defaultColorScheme="light">
|
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||||
<Notifications position="top-right" />
|
<Notifications position="top-right" />
|
||||||
{children}
|
{children}
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
|
|||||||
@@ -13,9 +13,69 @@
|
|||||||
document.documentElement.setAttribute('data-mantine-color-scheme', s);
|
document.documentElement.setAttribute('data-mantine-color-scheme', s);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Boot splash — shown until React mounts into #root. Colors are
|
||||||
|
hardcoded (not CSS vars from portal.css) so the splash never depends
|
||||||
|
on the app's own stylesheet finishing its load. */
|
||||||
|
#ema-boot-splash {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f8fc;
|
||||||
|
}
|
||||||
|
html[data-mantine-color-scheme='dark'] #ema-boot-splash {
|
||||||
|
background: #0e1521;
|
||||||
|
}
|
||||||
|
#ema-boot-splash svg {
|
||||||
|
width: 140px;
|
||||||
|
height: auto;
|
||||||
|
color: #3160b7;
|
||||||
|
animation: ema-boot-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ema-boot-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.55; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#ema-boot-splash svg { animation: none; }
|
||||||
|
}
|
||||||
|
/* React has committed into #root once it has a child; hide the splash. */
|
||||||
|
#root:not(:empty) ~ #ema-boot-splash {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
Static boot splash. Kept intentionally minimal (hull, funnel, mast,
|
||||||
|
flag — no cargo containers, no animated water) since this markup can't
|
||||||
|
import React. Source of truth for the full animated version is
|
||||||
|
libs/ui/src/lib/components/MaritimeLoader.tsx.
|
||||||
|
-->
|
||||||
|
<div id="ema-boot-splash" role="status" aria-live="polite" aria-label="Loading">
|
||||||
|
<svg viewBox="0 0 260 138" aria-hidden="true" focusable="false">
|
||||||
|
<ellipse cx="132" cy="113" rx="78" ry="7" fill="currentColor" opacity="0.12" />
|
||||||
|
<path d="M42 72H214l-4 6H48z" fill="currentColor" opacity="0.82" />
|
||||||
|
<path d="M166 25h10l3 17h-15z" fill="#eef3f5" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
|
||||||
|
<path d="M166.9 29h9.9l.6 3.5h-11.1z" fill="#078930" />
|
||||||
|
<path d="M166.2 32.5h11.2l.6 3.5h-12.4z" fill="#fcd116" />
|
||||||
|
<path d="M165.6 36h12.4l.6 3.5h-13.6z" fill="#da121a" />
|
||||||
|
<path d="M194 31V12M188 20h12M194 13l11 8M194 13l-9 8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<circle cx="194" cy="11" r="2" fill="currentColor" />
|
||||||
|
<path d="M184 20V8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||||
|
<path d="M184 8c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#078930" />
|
||||||
|
<path d="M184 12c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#fcd116" />
|
||||||
|
<path d="M184 16c7-3 11 3 18 0v4c-7 3-11-3-18 0z" fill="#da121a" />
|
||||||
|
<path d="M28 76h205l-15 18c-8 10-20 15-33 15H66c-13 0-24-5-31-15L23 80c-2-2 0-4 5-4z" fill="#f8fbfc" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" />
|
||||||
|
<path d="M34 88h190l-6 6c-8 10-20 15-33 15H66c-13 0-24-5-31-15z" fill="currentColor" opacity="0.92" />
|
||||||
|
<path d="M36 87h188" fill="none" stroke="rgb(255 255 255 / 52%)" stroke-width="2.4" stroke-linecap="round" />
|
||||||
|
<path d="M206 81l15 1-8 9" fill="none" stroke="rgb(255 255 255 / 46%)" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export function DocumentSlots({
|
|||||||
variant={uploaded ? 'light' : 'filled'}
|
variant={uploaded ? 'light' : 'filled'}
|
||||||
leftSection={
|
leftSection={
|
||||||
busy === requirement.key ? (
|
busy === requirement.key ? (
|
||||||
<Loader size={12} />
|
<Loader size={12} type="oval" />
|
||||||
) : (
|
) : (
|
||||||
<IconFileUpload size={14} />
|
<IconFileUpload size={14} />
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function StaffEvidence({ staffId, evidence, readOnly, onUploaded }: Props
|
|||||||
disabled={readOnly || busy === item.docKey}
|
disabled={readOnly || busy === item.docKey}
|
||||||
leftSection={
|
leftSection={
|
||||||
busy === item.docKey ? (
|
busy === item.docKey ? (
|
||||||
<Loader size={10} />
|
<Loader size={10} type="oval" />
|
||||||
) : uploaded ? (
|
) : uploaded ? (
|
||||||
<IconCheck size={12} />
|
<IconCheck size={12} />
|
||||||
) : undefined
|
) : undefined
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function OperationsFormContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loadingTypes || loadingMine) {
|
if (loadingTypes || loadingMine) {
|
||||||
return <Loader size="sm" />;
|
return <Loader size="sm" type="oval" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedNames = options
|
const removedNames = options
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core';
|
import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core';
|
||||||
import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form';
|
import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AmharicDatePicker } from '@ema-platform/ui';
|
import { AmharicDatePicker } from '@ema-platform/ui';
|
||||||
|
|
||||||
export const profileSchema = z.object({
|
export const profileSchema = z.object({
|
||||||
@@ -39,11 +40,13 @@ export function ProfileFormContent({
|
|||||||
professionsLoading,
|
professionsLoading,
|
||||||
professionOptions,
|
professionOptions,
|
||||||
}: ProfileFormContentProps) {
|
}: ProfileFormContentProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||||
<Select
|
<Select
|
||||||
label="Profession"
|
label="Profession"
|
||||||
placeholder={professionsLoading ? 'Loading...' : 'Select'}
|
placeholder={professionsLoading ? t('common.loading', 'Loading…') : 'Select'}
|
||||||
required
|
required
|
||||||
data={professionOptions}
|
data={professionOptions}
|
||||||
error={errors.professionId?.message}
|
error={errors.professionId?.message}
|
||||||
@@ -53,7 +56,7 @@ export function ProfileFormContent({
|
|||||||
name="professionId"
|
name="professionId"
|
||||||
searchable
|
searchable
|
||||||
disabled={professionsLoading}
|
disabled={professionsLoading}
|
||||||
rightSection={professionsLoading ? <Loader size="xs" /> : undefined}
|
rightSection={professionsLoading ? <Loader size="xs" type="oval" /> : undefined}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="First Name"
|
label="First Name"
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ function EvidenceModal({
|
|||||||
<Modal opened={Boolean(ownerId)} onClose={onClose} title="Evidence" centered>
|
<Modal opened={Boolean(ownerId)} onClose={onClose} title="Evidence" centered>
|
||||||
<Stack>
|
<Stack>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader size="sm" />
|
<Loader size="sm" type="oval" />
|
||||||
) : files.length === 0 ? (
|
) : files.length === 0 ? (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
No evidence uploaded yet.
|
No evidence uploaded yet.
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider, mergeThemeOverrides } from '@mantine/core';
|
||||||
import { Notifications } from '@mantine/notifications';
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
import { maritimeLoaderTheme } from '@ema-platform/ui';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { portalTheme } from '../theme/portalTheme';
|
import { portalTheme } from '../theme/portalTheme';
|
||||||
|
|
||||||
|
const theme = mergeThemeOverrides(portalTheme, maritimeLoaderTheme);
|
||||||
|
|
||||||
export function MantineThemeProvider({ children }: { children: ReactNode }) {
|
export function MantineThemeProvider({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<MantineProvider theme={portalTheme} defaultColorScheme="light">
|
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||||
<Notifications position="top-right" />
|
<Notifications position="top-right" />
|
||||||
{children}
|
{children}
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, type ReactNode } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { PageLoader } from '@ema-platform/ui';
|
||||||
import { authStorage } from '../utils/auth-storage';
|
import { authStorage } from '../utils/auth-storage';
|
||||||
import { hydrateAuth, logout, setUser } from '../store/auth.slice';
|
import { hydrateAuth, logout, setUser } from '../store/auth.slice';
|
||||||
import type { AuthUser } from '../types/auth.types';
|
import type { AuthUser } from '../types/auth.types';
|
||||||
@@ -70,7 +71,7 @@ export function AuthBootstrap({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Rendering the router before the session resolves would let the guards
|
// Rendering the router before the session resolves would let the guards
|
||||||
// redirect based on a state that is about to change.
|
// redirect based on a state that is about to change.
|
||||||
if (!ready) return null;
|
if (!ready) return <PageLoader />;
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export * from "./lib/feedback/notify";
|
|||||||
export * from "./lib/feedback/FeatureUnavailable";
|
export * from "./lib/feedback/FeatureUnavailable";
|
||||||
export * from "./lib/feedback/EmptyState";
|
export * from "./lib/feedback/EmptyState";
|
||||||
export * from "./lib/feedback/ErrorState";
|
export * from "./lib/feedback/ErrorState";
|
||||||
|
export * from "./lib/feedback/PageLoader";
|
||||||
|
export * from "./lib/components/MaritimeLoader";
|
||||||
|
export * from "./lib/theme/maritime-loader-theme";
|
||||||
export * from "./lib/layout/AppHeader";
|
export * from "./lib/layout/AppHeader";
|
||||||
export * from "./lib/layout/AppSidebar";
|
export * from "./lib/layout/AppSidebar";
|
||||||
export * from "./lib/layout/AppTopNav";
|
export * from "./lib/layout/AppTopNav";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CSSProperties, ComponentPropsWithoutRef } from "react";
|
import type { CSSProperties, ComponentPropsWithoutRef, Ref } from "react";
|
||||||
|
|
||||||
export interface MaritimeLoaderProps extends Omit<
|
export interface MaritimeLoaderProps extends Omit<
|
||||||
ComponentPropsWithoutRef<"span">,
|
ComponentPropsWithoutRef<"span">,
|
||||||
@@ -8,8 +8,9 @@ export interface MaritimeLoaderProps extends Omit<
|
|||||||
size?: number | string;
|
size?: number | string;
|
||||||
/** Standalone CSS color. Mantine's Loader color is used automatically when omitted. */
|
/** Standalone CSS color. Mantine's Loader color is used automatically when omitted. */
|
||||||
color?: string;
|
color?: string;
|
||||||
/** Accessible status text. */
|
/** Accessible status text (aria-label only — renders no visible text). */
|
||||||
label?: string;
|
label?: string;
|
||||||
|
ref?: Ref<HTMLSpanElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = `
|
const styles = `
|
||||||
@@ -231,15 +232,6 @@ const styles = `
|
|||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ema-loader__label {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: currentColor;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function toCssSize(value: number | string | undefined) {
|
function toCssSize(value: number | string | undefined) {
|
||||||
@@ -252,6 +244,7 @@ export function MaritimeLoader({
|
|||||||
label = "Loading maritime services",
|
label = "Loading maritime services",
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
ref,
|
||||||
...props
|
...props
|
||||||
}: MaritimeLoaderProps) {
|
}: MaritimeLoaderProps) {
|
||||||
const cssVariables = {
|
const cssVariables = {
|
||||||
@@ -263,12 +256,15 @@ export function MaritimeLoader({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={ref}
|
||||||
className={["ema-loader", className].filter(Boolean).join(" ")}
|
className={["ema-loader", className].filter(Boolean).join(" ")}
|
||||||
style={cssVariables}
|
style={cssVariables}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<style>{styles}</style>
|
<style href="ema-maritime-loader" precedence="low">
|
||||||
|
{styles}
|
||||||
|
</style>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
className="ema-loader__svg"
|
className="ema-loader__svg"
|
||||||
@@ -419,18 +415,8 @@ export function MaritimeLoader({
|
|||||||
<path d="M200 108c14 1 24-1 35-5" />
|
<path d="M200 108c14 1 24-1 35-5" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
{label && <span className="ema-loader__label">{label}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MaritimeLoader;
|
export default MaritimeLoader;
|
||||||
|
|
||||||
// how to use this component.
|
|
||||||
// <Center style={{ width: "90vw", height: "90vh" }}>
|
|
||||||
// <MaritimeLoader
|
|
||||||
// size={120}
|
|
||||||
// color="#075985"
|
|
||||||
// label="Loading Maritime Services..."
|
|
||||||
// />
|
|
||||||
// </Center>
|
|
||||||
|
|||||||
26
libs/ui/src/lib/feedback/PageLoader.tsx
Normal file
26
libs/ui/src/lib/feedback/PageLoader.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Center, Loader, Stack, Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface PageLoaderProps {
|
||||||
|
/** Visible + accessible label. Defaults to the shared i18n "loading" string. */
|
||||||
|
label?: string;
|
||||||
|
/** Container height. Defaults to a full-page fill. */
|
||||||
|
height?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The full-page/section loading state every screen should use instead of a bare `<Loader />`. */
|
||||||
|
export function PageLoader({ label, height = '60vh' }: PageLoaderProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const resolvedLabel = label ?? t('loading', 'Loading…');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center h={height}>
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Loader size={64} aria-label={resolvedLabel} />
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{resolvedLabel}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
libs/ui/src/lib/theme/maritime-loader-theme.ts
Normal file
31
libs/ui/src/lib/theme/maritime-loader-theme.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Button, createTheme, Loader, type MantineLoaderComponent } from '@mantine/core';
|
||||||
|
import { MaritimeLoader } from '../components/MaritimeLoader';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the branded ship loader as the default Mantine `Loader` type, and
|
||||||
|
* opts `Button`'s inline loading state back out to the stock oval — a 1.88:1
|
||||||
|
* ship does not fit a button's loader slot.
|
||||||
|
*
|
||||||
|
* Merge into an app theme with `mergeThemeOverrides(appTheme, maritimeLoaderTheme)`
|
||||||
|
* rather than importing this in `libs/shared`: `libs/ui` already depends on
|
||||||
|
* `libs/shared`, so the reverse import would be circular.
|
||||||
|
*/
|
||||||
|
export const maritimeLoaderTheme = createTheme({
|
||||||
|
components: {
|
||||||
|
Loader: Loader.extend({
|
||||||
|
defaultProps: {
|
||||||
|
// Mantine types custom loaders as ForwardRefExoticComponent; a plain
|
||||||
|
// function component satisfies the runtime contract (it forwards a
|
||||||
|
// ref as a normal prop under React 19) but not the structural type.
|
||||||
|
loaders: {
|
||||||
|
...Loader.defaultLoaders,
|
||||||
|
maritime: MaritimeLoader as unknown as MantineLoaderComponent,
|
||||||
|
},
|
||||||
|
type: 'maritime',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Button: Button.extend({
|
||||||
|
defaultProps: { loaderProps: { type: 'oval' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user