From 9a7b93cfb6df590c9ed08a4bf2b5e66b4e0b1b63 Mon Sep 17 00:00:00 2001
From: Estifo77 <139631617+Estifo77@users.noreply.github.com>
Date: Fri, 14 Aug 2026 14:45:57 +0300
Subject: [PATCH] loader UI improvements
---
apps/backoffice/index.html | 116 ++--
apps/portal/index.html | 116 ++--
libs/ui/src/lib/components/MaritimeLoader.tsx | 557 +++++++-----------
libs/ui/src/lib/feedback/PageLoader.tsx | 125 +++-
4 files changed, 499 insertions(+), 415 deletions(-)
diff --git a/apps/backoffice/index.html b/apps/backoffice/index.html
index f473aea4c..851a44f30 100644
--- a/apps/backoffice/index.html
+++ b/apps/backoffice/index.html
@@ -22,27 +22,75 @@
inset: 0;
z-index: 9999;
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
- background: #fff;
+ background: #0f172a;
+ color: #38bdf8;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
- html[data-mantine-color-scheme='dark'] #ema-boot-splash {
- background: #242424;
+ html[data-mantine-color-scheme='light'] #ema-boot-splash {
+ background: #f8fafc;
+ color: #0f2c59;
+ }
+ #ema-boot-splash .ema-card {
+ padding: 2.5rem 3.5rem;
+ border-radius: 1.5rem;
+ background: rgba(15, 23, 42, 0.85);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.35);
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ html[data-mantine-color-scheme='light'] #ema-boot-splash .ema-card {
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(15, 44, 89, 0.1);
+ box-shadow: 0 25px 50px -12px rgba(11, 25, 44, 0.12);
}
#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; }
+ .ema-boot-compass {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-boot-spin 20s linear infinite;
+ }
+ .ema-boot-helm {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-boot-spin-rev 14s linear infinite;
+ }
+ .ema-boot-title {
+ margin-top: 1rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ background: linear-gradient(135deg, #078930, #fcd116, #2563eb);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+ .ema-boot-sub {
+ margin-top: 0.25rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ opacity: 0.85;
+ }
+ @keyframes ema-boot-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+ @keyframes ema-boot-spin-rev {
+ from { transform: rotate(360deg); }
+ to { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
- #ema-boot-splash svg { animation: none; }
+ .ema-boot-compass, .ema-boot-helm { animation: none; }
}
- /* React has committed into #root once it has a child; hide the splash. */
+ /* Hide splash once React mounts */
#root:not(:empty) ~ #ema-boot-splash {
display: none;
}
@@ -50,31 +98,29 @@
-
-
-
+
+
+
+
+

+
+
ETHIOPIAN MARITIME AUTHORITY
+
የኢትዮጵያ ማሪታይም ባለስልጣን
+
Loading Maritime Backoffice…
+
diff --git a/apps/portal/index.html b/apps/portal/index.html
index 66a56efb0..0cb9e6cd8 100644
--- a/apps/portal/index.html
+++ b/apps/portal/index.html
@@ -22,27 +22,75 @@
inset: 0;
z-index: 9999;
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
- background: #f5f8fc;
+ background: #0f172a;
+ color: #38bdf8;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
- html[data-mantine-color-scheme='dark'] #ema-boot-splash {
- background: #0e1521;
+ html[data-mantine-color-scheme='light'] #ema-boot-splash {
+ background: #f8fafc;
+ color: #0f2c59;
+ }
+ #ema-boot-splash .ema-card {
+ padding: 2.5rem 3.5rem;
+ border-radius: 1.5rem;
+ background: rgba(15, 23, 42, 0.85);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.35);
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ html[data-mantine-color-scheme='light'] #ema-boot-splash .ema-card {
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(15, 44, 89, 0.1);
+ box-shadow: 0 25px 50px -12px rgba(11, 25, 44, 0.12);
}
#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; }
+ .ema-boot-compass {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-boot-spin 20s linear infinite;
+ }
+ .ema-boot-helm {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-boot-spin-rev 14s linear infinite;
+ }
+ .ema-boot-title {
+ margin-top: 1rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ background: linear-gradient(135deg, #078930, #fcd116, #2563eb);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+ .ema-boot-sub {
+ margin-top: 0.25rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ opacity: 0.85;
+ }
+ @keyframes ema-boot-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+ @keyframes ema-boot-spin-rev {
+ from { transform: rotate(360deg); }
+ to { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
- #ema-boot-splash svg { animation: none; }
+ .ema-boot-compass, .ema-boot-helm { animation: none; }
}
- /* React has committed into #root once it has a child; hide the splash. */
+ /* Hide splash once React mounts */
#root:not(:empty) ~ #ema-boot-splash {
display: none;
}
@@ -50,31 +98,29 @@
-
-
-
+
+
+
+
+

+
+
ETHIOPIAN MARITIME AUTHORITY
+
የኢትዮጵያ ማሪታይም ባለስልጣን
+
Loading Maritime Portal…
+
diff --git a/libs/ui/src/lib/components/MaritimeLoader.tsx b/libs/ui/src/lib/components/MaritimeLoader.tsx
index e5ab88cc0..d4c916e22 100644
--- a/libs/ui/src/lib/components/MaritimeLoader.tsx
+++ b/libs/ui/src/lib/components/MaritimeLoader.tsx
@@ -4,235 +4,138 @@ export interface MaritimeLoaderProps extends Omit<
ComponentPropsWithoutRef<"span">,
"children" | "color"
> {
- /** Standalone size. Mantine's Loader size is used automatically when omitted. */
+ /** Size in pixels or CSS string. Defaults to 80. */
size?: number | string;
- /** Standalone CSS color. Mantine's Loader color is used automatically when omitted. */
+ /** Primary accent color. Defaults to EMA Ocean Blue (#0284C7). */
color?: string;
- /** Accessible status text (aria-label only — renders no visible text). */
+ /** Visual variant: 'full' (emblem + orbital radar) or 'compact' (sleek inline spinner). */
+ variant?: "full" | "compact";
+ /** Accessible label. */
label?: string;
ref?: Ref
;
}
const styles = `
- .ema-loader {
+ .ema-pro-loader {
position: relative;
- display: inline-block;
- width: calc(var(--ema-size, var(--loader-size, 80px)) * 1.88);
- color: var(--ema-color, var(--loader-color, #075985));
- vertical-align: middle;
- }
-
- .ema-loader__svg {
- display: block;
- width: 100%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--ema-size, var(--loader-size, 80px));
height: var(--ema-size, var(--loader-size, 80px));
+ vertical-align: middle;
+ user-select: none;
+ }
+
+ .ema-pro-loader--full {
+ width: calc(var(--ema-size, var(--loader-size, 80px)) * 1.15);
+ height: calc(var(--ema-size, var(--loader-size, 80px)) * 1.15);
+ }
+
+ .ema-pro-loader__stage {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* --- Glowing Ambient Backdrop --- */
+ .ema-pro-loader__glow {
+ position: absolute;
+ inset: 10%;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(2, 132, 199, 0.28) 0%, rgba(14, 165, 233, 0.08) 55%, transparent 75%);
+ filter: blur(8px);
+ animation: ema-glow-pulse 3s ease-in-out infinite alternate;
+ }
+
+ /* --- SVG Overlay & Ring Animations --- */
+ .ema-pro-loader__svg {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
overflow: visible;
- filter: drop-shadow(
- 0 calc(var(--ema-size, var(--loader-size, 80px)) * 0.035)
- calc(var(--ema-size, var(--loader-size, 80px)) * 0.04)
- rgb(7 39 58 / 18%)
- );
}
- .ema-loader__ship {
- transform-box: fill-box;
- transform-origin: 50% 82%;
- animation: ema-ship-float 2.55s cubic-bezier(0.45, 0, 0.55, 1) infinite;
- }
-
- .ema-loader__shadow {
- fill: currentColor;
- opacity: 0.12;
+ .ema-pro-loader__ring-outer {
transform-box: fill-box;
transform-origin: center;
- animation: ema-shadow-breathe 2.55s cubic-bezier(0.45, 0, 0.55, 1) infinite;
+ animation: ema-spin-cw 16s linear infinite;
}
- .ema-loader__deck {
- fill: currentColor;
- opacity: 0.82;
- }
-
- .ema-loader__superstructure > path:first-child,
- .ema-loader__bridge-top {
- fill: #f8fbfc;
- stroke: currentColor;
- stroke-width: 2.4;
- stroke-linejoin: round;
- }
-
- .ema-loader__bridge-top { stroke-width: 2; }
-
- .ema-loader__window {
- fill: #bfe9ff;
- stroke: currentColor;
- stroke-width: 0.7;
- }
-
- .ema-loader__cabin-line {
- fill: currentColor;
- opacity: 0.35;
- }
-
- .ema-loader__funnel > path:first-child {
- fill: #eef3f5;
- stroke: currentColor;
- stroke-width: 2;
- stroke-linejoin: round;
- }
-
- .ema-loader__green { fill: #078930; }
- .ema-loader__yellow { fill: #fcd116; }
- .ema-loader__red { fill: #da121a; }
-
- .ema-loader__mast {
- fill: currentColor;
- stroke: currentColor;
- stroke-width: 1.8;
- stroke-linecap: round;
- stroke-linejoin: round;
- }
-
- .ema-loader__flag-pole {
- fill: none;
- stroke: currentColor;
- stroke-width: 1.7;
- stroke-linecap: round;
- }
-
- .ema-loader__flag {
+ .ema-pro-loader__ring-inner {
transform-box: fill-box;
- transform-origin: left center;
- animation: ema-flag-wave 0.95s ease-in-out infinite alternate;
+ transform-origin: center;
+ animation: ema-spin-ccw 10s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
- .ema-loader__hull {
- fill: #f8fbfc;
- stroke: currentColor;
- stroke-width: 1.4;
- stroke-linejoin: round;
+ .ema-pro-loader__sonar-wave {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-sonar-expand 2.6s cubic-bezier(0.1, 0.8, 0.3, 1) infinite;
}
- .ema-loader__lower-hull {
- fill: currentColor;
- opacity: 0.92;
+ .ema-pro-loader__core-logo {
+ position: relative;
+ z-index: 2;
+ width: 54%;
+ height: 54%;
+ object-fit: contain;
+ filter: drop-shadow(0 4px 10px rgba(11, 25, 44, 0.25));
+ animation: ema-logo-float 3.5s ease-in-out infinite alternate;
}
- .ema-loader__waterline {
- fill: none;
- stroke: rgb(255 255 255 / 52%);
- stroke-width: 2.4;
- stroke-linecap: round;
+ .ema-pro-loader__center-sync {
+ transform-box: fill-box;
+ transform-origin: center;
+ animation: ema-spin-cw 3s linear infinite;
}
- .ema-loader__bow-highlight {
- fill: none;
- stroke: rgb(255 255 255 / 46%);
- stroke-width: 2.3;
- stroke-linecap: round;
- stroke-linejoin: round;
+ /* --- Keyframe Animations --- */
+ @keyframes ema-spin-cw {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
}
- .ema-loader__portholes {
- fill: #d7f2ff;
- stroke: currentColor;
- stroke-width: 0.8;
+ @keyframes ema-spin-ccw {
+ from { transform: rotate(360deg); }
+ to { transform: rotate(0deg); }
}
- .ema-loader__cargo path {
- fill: none;
- stroke: rgb(255 255 255 / 22%);
- stroke-width: 1;
+ @keyframes ema-glow-pulse {
+ 0% { transform: scale(0.92); opacity: 0.5; }
+ 100% { transform: scale(1.12); opacity: 0.9; }
}
- .ema-loader__container-dark rect { fill: #3f4a52; }
- .ema-loader__container-muted rect { fill: #7d8991; }
- .ema-loader__container-steel rect { fill: #59656d; }
- .ema-loader__container-light rect { fill: #aab2b8; }
- .ema-loader__container-medium rect { fill: #6c7880; }
-
- .ema-loader__water-back,
- .ema-loader__water-front,
- .ema-loader__foam {
- fill: none;
- stroke-linecap: round;
- stroke-linejoin: round;
+ @keyframes ema-sonar-expand {
+ 0% { transform: scale(0.65); opacity: 0.9; stroke-width: 3; }
+ 60% { opacity: 0.35; }
+ 100% { transform: scale(1.4); opacity: 0; stroke-width: 0.5; }
}
- .ema-loader__water-back {
- stroke: currentColor;
- stroke-width: 5;
- opacity: 0.28;
- stroke-dasharray: 58 12;
- animation: ema-water-back 2.8s linear infinite;
- }
-
- .ema-loader__water-front {
- stroke: currentColor;
- stroke-width: 6;
- opacity: 0.55;
- stroke-dasharray: 70 10;
- animation: ema-water-front 1.9s linear infinite;
- }
-
- .ema-loader__foam {
- stroke: rgb(255 255 255 / 78%);
- stroke-width: 3;
- stroke-dasharray: 11 7;
- animation: ema-foam-drift 1.65s linear infinite;
- }
-
- @keyframes ema-ship-float {
- 0%, 100% { transform: translateY(1.5px) rotate(-0.65deg); }
- 50% { transform: translateY(-3px) rotate(0.65deg); }
- }
-
- @keyframes ema-shadow-breathe {
- 0%, 100% { transform: scaleX(1.02); opacity: 0.14; }
- 50% { transform: scaleX(0.9); opacity: 0.08; }
- }
-
- @keyframes ema-flag-wave {
- from { transform: skewY(-3deg) scaleX(0.94); }
- to { transform: skewY(3deg) scaleX(1.04); }
- }
-
- @keyframes ema-water-back {
- to { stroke-dashoffset: -140; }
- }
-
- @keyframes ema-water-front {
- to { stroke-dashoffset: 160; }
- }
-
- @keyframes ema-foam-drift {
- to { stroke-dashoffset: -36; }
+ @keyframes ema-logo-float {
+ 0% { transform: translateY(1px) scale(0.98); }
+ 100% { transform: translateY(-2px) scale(1.02); }
}
@media (prefers-reduced-motion: reduce) {
- .ema-loader__ship,
- .ema-loader__shadow,
- .ema-loader__flag,
- .ema-loader__water-back,
- .ema-loader__water-front,
- .ema-loader__foam {
- animation: none;
+ .ema-pro-loader__ring-outer,
+ .ema-pro-loader__ring-inner,
+ .ema-pro-loader__sonar-wave,
+ .ema-pro-loader__core-logo,
+ .ema-pro-loader__glow,
+ .ema-pro-loader__center-sync {
+ animation: none !important;
}
}
-@media (forced-colors: active) {
- .ema-loader__green,
- .ema-loader__yellow,
- .ema-loader__red,
- .ema-loader__container-dark rect,
- .ema-loader__container-muted rect,
- .ema-loader__container-steel rect,
- .ema-loader__container-light rect,
- .ema-loader__container-medium rect {
- fill: currentColor;
- }
- }
- `;
+ [data-mantine-color-scheme='dark'] .ema-pro-loader__glow {
+ background: radial-gradient(circle, rgba(56, 189, 248, 0.35) 0%, rgba(2, 132, 199, 0.12) 60%, transparent 80%);
+ }
+`;
function toCssSize(value: number | string | undefined) {
return typeof value === "number" ? `${value}px` : value;
@@ -241,7 +144,8 @@ function toCssSize(value: number | string | undefined) {
export function MaritimeLoader({
size,
color,
- label = "Loading maritime services",
+ variant = "full",
+ label = "Ethiopian Maritime Authority Loading",
className,
style,
ref,
@@ -257,166 +161,151 @@ export function MaritimeLoader({
-
-
);
}
export default MaritimeLoader;
+
+
diff --git a/libs/ui/src/lib/feedback/PageLoader.tsx b/libs/ui/src/lib/feedback/PageLoader.tsx
index 6bc6c687e..120075ef8 100644
--- a/libs/ui/src/lib/feedback/PageLoader.tsx
+++ b/libs/ui/src/lib/feedback/PageLoader.tsx
@@ -1,26 +1,129 @@
-import { Center, Loader, Stack, Text } from '@mantine/core';
+import { Center, Box, Stack, Text, useMantineColorScheme } from '@mantine/core';
import { useTranslation } from 'react-i18next';
+import { MaritimeLoader } from '../components/MaritimeLoader';
interface PageLoaderProps {
/** Visible + accessible label. Defaults to the shared i18n "loading" string. */
label?: string;
+ /** Subtitle or secondary detail string. */
+ subtitle?: string;
/** Container height. Defaults to a full-page fill. */
height?: number | string;
+ /** Show framed executive card backdrop (default: true). */
+ framed?: boolean;
}
-/** The full-page/section loading state every screen should use instead of a bare ``. */
-export function PageLoader({ label, height = '60vh' }: PageLoaderProps) {
+/** The full-page/section loading state every screen should use. */
+export function PageLoader({
+ label,
+ subtitle,
+ height = '60vh',
+ framed = true,
+}: PageLoaderProps) {
const { t } = useTranslation();
- const resolvedLabel = label ?? t('loading', 'Loading…');
+ const { colorScheme } = useMantineColorScheme();
+ const isDark = colorScheme === 'dark';
+
+ const resolvedLabel = label ?? t('loading', 'Loading Maritime Services…');
return (
-
-
-
-
- {resolvedLabel}
-
-
+
+
+
+ {/* Main Maritime Animated Loader */}
+
+
+ {/* Ethiopian Maritime Authority Branding Header */}
+
+
+ ETHIOPIAN MARITIME AUTHORITY
+
+
+ የኢትዮጵያ ማሪታይም ባለስልጣን
+
+
+ {resolvedLabel}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {/* Animated Gold Shimmer Bar */}
+
+
+
+
+
+
);
}
+