feat(ui): animated boot splash and loading screen

Show a Lottie-animated splash while the app bundle boots and dismiss it
once WebAppComponent initialises, plus a reusable mifosx-loading-screen
component with loading and error animations. lottie.min.js is served
from the lottie-web package instead of being vendored into src/assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 15:34:31 +05:30
co-authored by Claude Opus 5.5
parent 799a6930b8
commit 3928936b93
16 changed files with 6938 additions and 38 deletions
+7
View File
@@ -31,6 +31,8 @@
"@angular/localize/init"
],
"assets": [
"src/favicon.ico",
"src/apple-touch-icon.png",
"src/assets/images/cbsnexora-favicon.svg",
"src/robots.txt",
"src/manifest.json",
@@ -39,6 +41,11 @@
"glob": "**/*",
"input": "node_modules/tinymce",
"output": "assets/tinymce"
},
{
"glob": "lottie.min.js",
"input": "node_modules/lottie-web/build/player",
"output": "assets/js"
}
],
"styles": [
+7
View File
@@ -41,6 +41,7 @@
"jspdf-autotable": "^5.0.2",
"lightgallery": "^2.9.0",
"lodash": "4.17.23",
"lottie-web": "^5.13.0",
"moment": "^2.29.4",
"ngx-image-cropper": "^9.1.6",
"ngx-mat-select-search": "^8.0.2",
@@ -18380,6 +18381,12 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/lottie-web": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz",
"integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+1
View File
@@ -71,6 +71,7 @@
"jspdf-autotable": "^5.0.2",
"lightgallery": "^2.9.0",
"lodash": "4.17.23",
"lottie-web": "^5.13.0",
"moment": "^2.29.4",
"ngx-image-cropper": "^9.1.6",
"ngx-mat-select-search": "^8.0.2",
+2
View File
@@ -96,6 +96,7 @@ import { TokenInterceptor as ZitadelTokenInterceptor } from './zitadel/token.int
import { AuthService } from './zitadel/auth.service';
import { environment } from '../environments/environment';
import { CallbackComponent } from './zitadel/callback/callback.component';
import { LoadingScreenComponent } from './shared/loading-screen/loading-screen.component';
import { OAuthModule } from 'angular-oauth2-oidc';
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
@@ -195,6 +196,7 @@ export function HttpLoaderFactory(http: HttpClient) {
AppRoutingModule,
NotFoundComponent,
CallbackComponent,
LoadingScreenComponent,
OAuthModule.forRoot()
],
providers: [
@@ -0,0 +1,88 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
@if (active) {
@if (currentMode === 'overlay') {
<!-- Mifos Mobile Transaction / Action Progress Overlay -->
<div class="cbs-loading-backdrop" (click)="preventDismiss($event)">
<div class="cbs-loading-dialog" role="dialog" aria-modal="true" [attr.aria-label]="title || 'Loading'">
<div class="cbs-lottie-container" #lottieContainer>
<!-- Vector Fallback if Lottie is loading -->
@if (!lottieLoaded) {
<div class="cbs-vector-spinner">
<div class="spinner-ring ring-outer"></div>
<div class="spinner-ring ring-mid"></div>
<div class="spinner-ring ring-inner"></div>
</div>
}
</div>
<div class="cbs-loading-dialog__content">
<h3 class="cbs-loading-dialog__title">{{ title || 'CBS Nexora' }}</h3>
<p class="cbs-loading-dialog__message">{{ message || 'Please wait...' }}</p>
</div>
<div class="cbs-loading-pulse-bar">
<div class="cbs-pulse-bar__indicator"></div>
</div>
</div>
</div>
}
@if (currentMode === 'fullscreen') {
<!-- Full-Screen App Boot / Splash Screen -->
<div class="cbs-fullscreen-loader">
<div class="cbs-fullscreen-loader__brand">
<div class="cbs-brand-icon">
<svg width="40" height="40" viewBox="0 0 32 32" aria-hidden="true">
<rect x="1" y="1" width="14" height="14" rx="2" fill="#2563eb" />
<rect x="17" y="1" width="14" height="14" rx="2" fill="#0ea5e9" />
<rect x="1" y="17" width="14" height="14" rx="2" fill="#10b981" />
<rect x="17" y="17" width="14" height="14" rx="2" fill="#94a3b8" />
</svg>
</div>
<h2 class="cbs-brand-name">{{ title || 'CBS Nexora' }}</h2>
<span class="cbs-brand-sub">Core Banking System</span>
</div>
<div class="cbs-lottie-container cbs-lottie-container--large" #lottieContainer>
@if (!lottieLoaded) {
<div class="cbs-vector-spinner cbs-vector-spinner--large">
<div class="spinner-ring ring-outer"></div>
<div class="spinner-ring ring-mid"></div>
<div class="spinner-ring ring-inner"></div>
</div>
}
</div>
<div class="cbs-progress-track">
<div class="cbs-progress-fill"></div>
</div>
<p class="cbs-fullscreen-loader__status">{{ message || 'Connecting to Core Banking Engine...' }}</p>
</div>
}
@if (currentMode === 'inline') {
<!-- Inline Component Loader -->
<div class="cbs-inline-loader">
<div class="cbs-lottie-container cbs-lottie-container--compact" #lottieContainer>
@if (!lottieLoaded) {
<div class="cbs-vector-spinner cbs-vector-spinner--compact">
<div class="spinner-ring ring-outer"></div>
<div class="spinner-ring ring-mid"></div>
<div class="spinner-ring ring-inner"></div>
</div>
}
</div>
@if (message) {
<p class="cbs-inline-loader__message">{{ message }}</p>
}
</div>
}
}
@@ -0,0 +1,341 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
:host {
display: block;
}
/* ==========================================================================
Overlay Mode (MifosProgressIndicatorOverlay / MifosLoadingDialog)
========================================================================== */
.cbs-loading-backdrop {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
background: rgb(9 13 22 / 72%);
backdrop-filter: blur(8px);
animation: backdrop-fade-in 0.25s ease-out forwards;
}
.cbs-loading-dialog {
position: relative;
width: 90%;
max-width: 340px;
padding: 28px 24px;
background: #111827;
color: #f8fafc;
border-radius: 20px;
border: 1px solid rgb(255 255 255 / 12%);
box-shadow:
0 25px 60px -15px rgb(0 0 0 / 65%),
0 0 35px rgb(37 99 235 / 18%);
text-align: center;
overflow: hidden;
animation: dialog-scale-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
:host-context([data-theme='light']) .cbs-loading-dialog,
:host-context(.light-theme) .cbs-loading-dialog {
background: #fff;
color: #0f172a;
border-color: rgb(0 0 0 / 8%);
box-shadow:
0 25px 50px -12px rgb(0 0 0 / 25%),
0 0 25px rgb(37 99 235 / 12%);
}
.cbs-loading-dialog__content {
margin-top: 10px;
margin-bottom: 16px;
}
.cbs-loading-dialog__title {
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
.cbs-loading-dialog__message {
font-size: 0.82rem;
color: #94a3b8;
line-height: 1.45;
margin: 0;
}
:host-context([data-theme='light']) .cbs-loading-dialog__message,
:host-context(.light-theme) .cbs-loading-dialog__message {
color: #64748b;
}
/* Pulse bar at dialog bottom */
.cbs-loading-pulse-bar {
width: 100%;
height: 3px;
background: rgb(255 255 255 / 8%);
border-radius: 99px;
overflow: hidden;
position: relative;
margin-top: 6px;
}
.cbs-pulse-bar__indicator {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 40%;
background: linear-gradient(90deg, #2563eb, #38bdf8);
border-radius: 99px;
animation: pulse-bar-slide 1.4s infinite ease-in-out;
}
/* ==========================================================================
Full-Screen Mode (App Boot / Splash)
========================================================================== */
.cbs-fullscreen-loader {
position: fixed;
inset: 0;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(circle at center, #0f172a 0%, #090d16 100%);
color: #f8fafc;
padding: 24px;
text-align: center;
}
:host-context([data-theme='light']) .cbs-fullscreen-loader,
:host-context(.light-theme) .cbs-fullscreen-loader {
background: radial-gradient(circle at center, #fff 0%, #f1f5f9 100%);
color: #0f172a;
}
.cbs-fullscreen-loader__brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.cbs-brand-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: linear-gradient(135deg, #1e3a8a 0%, #0284c7 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 24px rgb(14 165 233 / 35%);
margin-bottom: 6px;
}
.cbs-brand-name {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0;
}
.cbs-brand-sub {
font-size: 0.8rem;
color: #94a3b8;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.cbs-progress-track {
width: 240px;
height: 5px;
background: rgb(255 255 255 / 10%);
border-radius: 99px;
overflow: hidden;
margin: 18px 0 12px;
position: relative;
}
.cbs-progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 45%;
background: linear-gradient(90deg, #2563eb, #38bdf8);
border-radius: 99px;
box-shadow: 0 0 12px rgb(56 189 248 / 50%);
animation: pulse-bar-slide 1.5s infinite ease-in-out;
}
.cbs-fullscreen-loader__status {
font-size: 0.82rem;
color: #94a3b8;
font-family: inherit;
margin: 0;
}
/* ==========================================================================
Inline Mode
========================================================================== */
.cbs-inline-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
text-align: center;
}
.cbs-inline-loader__message {
font-size: 0.82rem;
color: #94a3b8;
margin-top: 10px;
}
/* ==========================================================================
Lottie & Vector Spinner Dimensions
========================================================================== */
.cbs-lottie-container {
width: 110px;
height: 110px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
}
.cbs-lottie-container--large {
width: 140px;
height: 140px;
}
.cbs-lottie-container--compact {
width: 64px;
height: 64px;
}
/* Vector Spinner Fallback (CSS Multi-Ring Loader) */
.cbs-vector-spinner {
width: 72px;
height: 72px;
position: relative;
}
.cbs-vector-spinner--large {
width: 96px;
height: 96px;
}
.cbs-vector-spinner--compact {
width: 44px;
height: 44px;
}
.spinner-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid transparent;
}
.ring-outer {
border-top-color: #2563eb;
border-right-color: #2563eb;
animation: spin-outer 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
.ring-mid {
inset: 6px;
border-bottom-color: #0ea5e9;
border-left-color: #0ea5e9;
animation: spin-mid 1.6s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
}
.ring-inner {
inset: 13px;
border-top-color: #10b981;
animation: spin-inner 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
/* ==========================================================================
Keyframe Animations
========================================================================== */
@keyframes backdrop-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dialog-scale-in {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-bar-slide {
0% {
left: -40%;
width: 30%;
}
50% {
left: 35%;
width: 50%;
}
100% {
left: 100%;
width: 30%;
}
}
@keyframes spin-outer {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spin-mid {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spin-inner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,168 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import {
Component,
OnInit,
OnDestroy,
Input,
ElementRef,
ViewChild,
AfterViewInit,
inject,
ChangeDetectorRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subscription } from 'rxjs';
import { LoadingScreenService, LoadingConfig } from './loading-screen.service';
declare const lottie: any;
@Component({
selector: 'mifosx-loading-screen',
standalone: true,
imports: [CommonModule],
templateUrl: './loading-screen.component.html',
styleUrls: ['./loading-screen.component.scss']
})
export class LoadingScreenComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly loadingService = inject(LoadingScreenService);
private readonly cdr = inject(ChangeDetectorRef);
@Input() mode: 'overlay' | 'inline' | 'fullscreen' = 'overlay';
@Input() title: string = 'CBS Nexora';
@Input() message: string = 'Processing transaction...';
@Input() active: boolean = false;
@Input() isGlobalOverlay: boolean = true;
@ViewChild('lottieContainer') lottieContainerRef?: ElementRef<HTMLDivElement>;
currentMode: 'overlay' | 'inline' | 'fullscreen' = 'overlay';
lottieLoaded: boolean = false;
private animInstance: any = null;
private subscription?: Subscription;
ngOnInit(): void {
this.currentMode = this.mode;
if (this.isGlobalOverlay) {
this.subscription = new Subscription();
this.subscription.add(
this.loadingService.isVisible$.subscribe((visible) => {
this.active = visible;
this.cdr.markForCheck();
if (visible) {
setTimeout(() => this.initLottie(), 50);
} else {
this.destroyLottie();
}
})
);
this.subscription.add(
this.loadingService.config$.subscribe((cfg: LoadingConfig) => {
if (cfg.title !== undefined) this.title = cfg.title;
if (cfg.message !== undefined) this.message = cfg.message;
if (cfg.mode !== undefined) this.currentMode = cfg.mode;
this.cdr.markForCheck();
})
);
}
}
ngAfterViewInit(): void {
if (this.active) {
this.initLottie();
}
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.destroyLottie();
}
preventDismiss(event: MouseEvent): void {
event.stopPropagation();
}
private initLottie(): void {
if (!this.lottieContainerRef?.nativeElement) return;
this.destroyLottie();
const container = this.lottieContainerRef.nativeElement;
// Check if lottie-web global is available
if (typeof lottie !== 'undefined') {
try {
this.animInstance = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: true,
path: 'assets/animations/loading_animation.json'
});
this.lottieLoaded = true;
this.cdr.markForCheck();
return;
} catch (err) {
console.warn('Could not load Lottie animation, using vector fallback', err);
}
}
// Try dynamic script load of local asset if not loaded yet
this.loadLottieScript()
.then(() => {
if (typeof lottie !== 'undefined' && this.lottieContainerRef?.nativeElement) {
this.animInstance = lottie.loadAnimation({
container: this.lottieContainerRef.nativeElement,
renderer: 'svg',
loop: true,
autoplay: true,
path: 'assets/animations/loading_animation.json'
});
this.lottieLoaded = true;
this.cdr.markForCheck();
}
})
.catch(() => {
this.lottieLoaded = false;
this.cdr.markForCheck();
});
}
private loadLottieScript(): Promise<void> {
return new Promise((resolve, reject) => {
if (typeof lottie !== 'undefined') {
resolve();
return;
}
const existingScript = document.getElementById('cbs-lottie-script');
if (existingScript) {
existingScript.addEventListener('load', () => resolve());
return;
}
const script = document.createElement('script');
script.id = 'cbs-lottie-script';
script.src = 'assets/js/lottie.min.js';
script.onload = () => resolve();
script.onerror = () => reject();
document.head.appendChild(script);
});
}
private destroyLottie(): void {
if (this.animInstance) {
try {
this.animInstance.destroy();
} catch (_) {}
this.animInstance = null;
}
}
}
@@ -0,0 +1,61 @@
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface LoadingConfig {
title?: string;
message?: string;
mode?: 'overlay' | 'inline' | 'fullscreen';
}
@Injectable({
providedIn: 'root'
})
export class LoadingScreenService {
private readonly visibleSubject = new BehaviorSubject<boolean>(false);
private readonly configSubject = new BehaviorSubject<LoadingConfig>({
title: 'CBS Nexora',
message: 'Processing transaction...',
mode: 'overlay'
});
readonly isVisible$: Observable<boolean> = this.visibleSubject.asObservable();
readonly config$: Observable<LoadingConfig> = this.configSubject.asObservable();
/**
* Show the loading screen/overlay
* @param message Optional message to display (e.g. "Processing Transfer...")
* @param title Optional title to display (defaults to "CBS Nexora")
* @param mode Optional display mode ('overlay' | 'fullscreen')
*/
show(message?: string, title?: string, mode: 'overlay' | 'fullscreen' = 'overlay'): void {
this.configSubject.next({
title: title || 'CBS Nexora',
message: message || 'Please wait...',
mode
});
this.visibleSubject.next(true);
}
/**
* Hide the loading screen/overlay
*/
hide(): void {
this.visibleSubject.next(false);
}
/**
* Update message while loading screen is active
*/
setMessage(message: string): void {
const current = this.configSubject.getValue();
this.configSubject.next({ ...current, message });
}
}
+3
View File
@@ -30,6 +30,7 @@ import { SearchToolComponent } from './search-tool/search-tool.component';
import { KeyboardShortcutsDialogComponent } from './keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component';
import { ServerSelectorComponent } from './server-selector/server-selector.component';
import { TenantSelectorComponent } from './tenant-selector/tenant-selector.component';
import { LoadingScreenComponent } from './loading-screen/loading-screen.component';
/** Custom Modules */
import { IconsModule } from './icons.module';
@@ -116,9 +117,11 @@ import { DatetimePickerStubComponent } from './datetime-picker-stub/datetime-pic
DropdownComponent,
InputAmountComponent,
InputPasswordComponent,
LoadingScreenComponent,
DatetimePickerStubComponent // temporarily added stub for datetime picker
],
exports: [
LoadingScreenComponent,
FileUploadComponent,
FooterComponent,
LanguageSelectorComponent,
+1
View File
@@ -7,3 +7,4 @@
-->
<router-outlet></router-outlet>
<mifosx-loading-screen></mifosx-loading-screen>
+5
View File
@@ -296,6 +296,11 @@ export class WebAppComponent implements OnInit, OnDestroy {
});
});
}
// Dismiss initial application boot splash screen smoothly
if (typeof (window as any).hideAppSplash === 'function') {
(window as any).hideAppSplash();
}
}
ngOnDestroy() {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+72 -30
View File
@@ -6,53 +6,95 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/* ==========================================================================
Modernized Loader Wrapper (Inspired by openMF/mifos-mobile)
Supports full-screen and embedded containers with smooth vector physics.
========================================================================== */
.loader-wrapper {
height: 100vh;
width: 100vw;
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 280px;
height: 100%;
background: transparent;
/* Modern orbital rings that replace legacy rigid triangles smoothly */
.triangle {
width: 0;
height: 0;
border-style: solid;
position: absolute;
animation: rotate 0.8s infinite 0.2s;
border-radius: 50%;
border-style: solid;
box-sizing: border-box;
transition: all 0.3s ease;
}
.bottom {
border-width: 0 90px 90px;
border-color: transparent transparent #1fb050;
bottom: calc(50% - 100px);
left: calc(50% - 90px);
width: 72px;
height: 72px;
border-width: 3px;
border-color: #2563eb transparent transparent;
animation: cbs-spin-ring 1.1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
box-shadow: 0 0 16px rgb(37 99 235 / 35%);
}
.top {
border-width: 90px 90px 0;
border-color: #b4d575 transparent transparent;
top: calc(50% - 100px);
left: calc(50% - 90px);
width: 54px;
height: 54px;
border-width: 3px;
border-color: transparent transparent #0ea5e9;
animation: cbs-spin-ring 1.4s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
box-shadow: 0 0 14px rgb(14 165 233 / 35%);
}
.left {
border-width: 90px 0 90px 90px;
border-color: transparent transparent transparent #1074b9;
top: calc(50% - 90px);
left: calc(50% - 100px);
width: 36px;
height: 36px;
border-width: 2.5px;
border-color: #10b981 transparent transparent;
animation: cbs-spin-ring 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
box-shadow: 0 0 10px rgb(16 185 129 / 35%);
}
.right {
border-width: 90px 90px 90px 0;
border-color: transparent #1daeec transparent transparent;
top: calc(50% - 90px);
right: calc(50% - 100px);
width: 18px;
height: 18px;
border-width: 2px;
border-color: transparent #38bdf8 transparent transparent;
animation: cbs-spin-ring 0.7s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
/* Optional central glowing core */
&::after {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: #38bdf8;
box-shadow: 0 0 12px #38bdf8;
animation: cbs-pulse-core 1.5s ease-in-out infinite;
}
}
@keyframes cbs-spin-ring {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes cbs-pulse-core {
0%,
100% {
transform: scale(0.85);
opacity: 0.6;
}
50% {
transform: scale(1.3);
opacity: 1;
}
}
+259 -8
View File
@@ -11,7 +11,7 @@
<html lang="en" class="h-full bg-gray-100">
<head>
<meta charset="utf-8" />
<title>{{'APP_NAME' | translate}}</title>
<title>CBS Nexora</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
@@ -111,13 +111,264 @@
});
</script>
<mifosx-web-app>
<div class="loader-wrapper">
<div class="bottom triangle"></div>
<div class="top triangle"></div>
<div class="left triangle"></div>
<div class="right triangle"></div>
<div id="cbs-app-splash-screen" class="cbs-init-screen">
<style>
.cbs-init-screen {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(circle at center, #0f172a 0%, #090d16 100%);
color: #f8fafc;
font-family:
'Plus Jakarta Sans',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
z-index: 999999;
overflow: hidden;
transition:
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.cbs-init-screen.cbs-splash-fade-out {
opacity: 0;
transform: scale(1.02);
pointer-events: none;
}
.cbs-init-brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.cbs-init-icon {
width: 72px;
height: 72px;
border-radius: 18px;
background: linear-gradient(135deg, #1e3a8a 0%, #0284c7 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12px 28px rgba(14, 165, 233, 0.35);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.cbs-init-title {
font-size: 1.6rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0;
}
.cbs-init-sub {
font-size: 0.8rem;
color: #94a3b8;
font-weight: 500;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.cbs-init-lottie-box {
width: 130px;
height: 130px;
margin: 8px auto;
display: flex;
align-items: center;
justify-content: center;
filter: drop-shadow(0 8px 24px rgba(37, 99, 235, 0.25));
}
.cbs-init-spinner {
width: 64px;
height: 64px;
position: relative;
}
.cbs-init-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid transparent;
}
.cbs-init-ring-1 {
border-top-color: #2563eb;
border-right-color: #2563eb;
animation: cbsInitSpin 1.1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
.cbs-init-ring-2 {
inset: 6px;
border-bottom-color: #0ea5e9;
border-left-color: #0ea5e9;
animation: cbsInitSpin 1.5s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
}
.cbs-init-ring-3 {
inset: 12px;
border-top-color: #10b981;
animation: cbsInitSpin 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
.cbs-init-track {
width: 260px;
height: 5px;
background: rgba(255, 255, 255, 0.1);
border-radius: 99px;
overflow: hidden;
margin: 18px 0 12px;
position: relative;
}
.cbs-init-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #2563eb, #38bdf8);
border-radius: 99px;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
transition: width 0.3s ease;
}
.cbs-init-status {
font-size: 0.82rem;
color: #94a3b8;
font-family: inherit;
margin: 0;
height: 20px;
}
@keyframes cbsInitSpin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<div class="cbs-init-brand">
<div class="cbs-init-icon">
<svg width="40" height="40" viewBox="0 0 32 32">
<rect x="2" y="2" width="13" height="13" rx="2.5" fill="#ffffff" />
<rect x="17" y="2" width="13" height="13" rx="2.5" fill="#29abe2" />
<rect x="2" y="17" width="13" height="13" rx="2.5" fill="#00b388" />
<rect x="17" y="17" width="13" height="13" rx="2.5" fill="#6b7fa3" />
</svg>
</div>
<h2 class="cbs-init-title">CBS Nexora</h2>
<span class="cbs-init-sub">Next-Generation Core Banking</span>
</div>
</mifosx-web-app>
<div id="cbs-init-lottie-target" class="cbs-init-lottie-box">
<div class="cbs-init-spinner">
<div class="cbs-init-ring cbs-init-ring-1"></div>
<div class="cbs-init-ring cbs-init-ring-2"></div>
<div class="cbs-init-ring cbs-init-ring-3"></div>
</div>
</div>
<div class="cbs-init-track">
<div class="cbs-init-fill" id="cbs-init-progress-bar"></div>
</div>
<p class="cbs-init-status" id="cbs-init-status-text">Connecting to Secure Banking Gateway...</p>
</div>
<script>
(function () {
var splash = document.getElementById('cbs-app-splash-screen');
var bar = document.getElementById('cbs-init-progress-bar');
var status = document.getElementById('cbs-init-status-text');
var progress = 0;
var minTimePassed = false;
var angularReady = false;
var dismissed = false;
// Load Lottie Vector Animation
var script = document.createElement('script');
script.src = 'assets/js/lottie.min.js';
script.onload = function () {
if (typeof lottie !== 'undefined') {
var target = document.getElementById('cbs-init-lottie-target');
if (target) {
target.innerHTML = '';
lottie.loadAnimation({
container: target,
renderer: 'svg',
loop: true,
autoplay: true,
path: 'assets/animations/loading_animation.json'
});
}
}
};
document.head.appendChild(script);
// Progress simulation stages (Guaranteed ~2 seconds duration on refresh)
var interval = setInterval(function () {
if (progress < 90) {
progress += 5;
} else if (angularReady && progress < 100) {
progress += 5;
}
if (bar) bar.style.width = Math.min(progress, 100) + '%';
if (status) {
if (progress >= 85) {
status.innerText = 'Launching CBS Nexora Dashboard...';
} else if (progress >= 60) {
status.innerText = 'Synchronizing core banking ledger schema...';
} else if (progress >= 30) {
status.innerText = 'Authenticating token & cryptographic keys...';
}
}
if (progress >= 100 && angularReady && minTimePassed) {
clearInterval(interval);
dismissSplash();
}
}, 90);
// Enforce minimum hold time (2000ms = 2 seconds) so refresh always shows the animation
setTimeout(function () {
minTimePassed = true;
if (angularReady) {
progress = 100;
if (bar) bar.style.width = '100%';
dismissSplash();
}
}, 2000);
function dismissSplash() {
if (dismissed) return;
dismissed = true;
clearInterval(interval);
if (bar) bar.style.width = '100%';
if (status) status.innerText = 'Ready! System loaded successfully.';
setTimeout(function () {
if (splash) {
splash.classList.add('cbs-splash-fade-out');
setTimeout(function () {
if (splash && splash.parentNode) {
splash.parentNode.removeChild(splash);
}
}, 600);
}
}, 250);
}
// Global hook for Angular to notify when ready
window.hideAppSplash = function () {
angularReady = true;
if (minTimePassed) {
dismissSplash();
}
};
// Safety fallback (Max 7s in case of backend network failure)
setTimeout(function () {
angularReady = true;
minTimePassed = true;
dismissSplash();
}, 7000);
})();
</script>
<mifosx-web-app></mifosx-web-app>
</body>
</html>