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 @@
- -
- +
+
+
+ + + + + + + + + + + + + + + EMA +
+
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 @@
- -
- +
+
+
+ + + + + + + + + + + + + + + EMA +
+
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({ - -
+ {/* Soft Ambient Radial Light Aura */} +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {/* Precision Orbital Rings & Sonar Radar SVG */} + + {/* Inner Counter-Rotating Golden Ring */} + + + + + + {/* Vector EMA Emblem Backup for Instant Render */} + {variant === "compact" && ( + + + + + )} + + + {/* Official EMA Crest Logo Image */} + Ethiopian Maritime Authority { + // Fallback if image path is not resolving in dev iframe + (e.target as HTMLElement).style.display = "none"; + }} + /> +
); } 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 */} + + + + + +
); } +