Merge pull request #2921 from alberto-art3ch/WEB-492/material_design_3_initial_implementation

WEB-492: Material Design 3 implementation: Login
This commit is contained in:
Gopi Kishan
2026-01-07 10:37:56 +05:30
committed by GitHub
58 changed files with 3396 additions and 855 deletions
+6 -4
View File
@@ -15,9 +15,10 @@
}
],
"selector-attribute-quotes": "always",
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"selector-max-specificity": "1,3,2",
"max-nesting-depth": 7,
"selector-max-compound-selectors": 8,
"selector-max-specificity": null,
"no-descending-specificity": null,
"declaration-no-important": null,
"at-rule-no-vendor-prefix": true,
"at-rule-no-unknown": null,
@@ -46,7 +47,8 @@
"/^mat/",
"/^mifosx/",
"fa-icon",
"ng-template"
"ng-template",
"/^md-/"
]
}
],
+223 -425
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -51,6 +51,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@material/web": "^2.4.1",
"@ngx-translate/core": "^16.0.4",
"@ngx-translate/http-loader": "^16.0.1",
"@swimlane/ngx-graph": "^11.0.0",
+8 -3
View File
@@ -52,14 +52,19 @@ export default defineConfig({
screenshot: 'only-on-failure',
// Default navigation timeout (increased for CI and financial app)
navigationTimeout: 60000,
navigationTimeout: 120000,
// Default action timeout
actionTimeout: 20000
actionTimeout: 30000,
// Configure backend URL for tests
extraHTTPHeaders: {
Accept: 'application/json'
}
},
// Global test timeout (per test)
timeout: process.env.CI ? 90000 : 60000,
timeout: process.env.CI ? 180000 : 120000,
// Configure projects for different browsers
projects: [
+10 -9
View File
@@ -33,10 +33,10 @@ export class LoginPage extends BasePage {
/**
* Get the username input field.
* Uses codegen selector for the actual input.
* Uses formcontrolname attribute for reliable selection.
*/
get usernameInput(): Locator {
return this.page.getByRole('textbox', { name: 'Username' });
return this.page.locator('input[formcontrolname="username"]');
}
/**
@@ -49,10 +49,10 @@ export class LoginPage extends BasePage {
/**
* Get the password input field.
* Uses codegen selector for the actual input.
* Uses formcontrolname attribute for reliable selection.
*/
get passwordInput(): Locator {
return this.page.getByRole('textbox', { name: 'Password' });
return this.page.locator('input[formcontrolname="password"]');
}
/**
@@ -63,6 +63,10 @@ export class LoginPage extends BasePage {
return this.page.getByRole('button', { name: 'Login' });
}
public divLocator(className: string): Locator {
return this.page.locator(className);
}
// Additional helper locators for validation and assertions
private readonly locators = {
errorMessage: 'mat-error',
@@ -105,20 +109,17 @@ export class LoginPage extends BasePage {
/**
* Perform login with the given credentials.
* Uses the exact interaction sequence from codegen.
*
* @param username - The username to enter
* @param password - The password to enter
*/
async login(username: string, password: string): Promise<void> {
// Click the username wrapper div (as per codegen)
await this.usernameDivWrapper.click();
// Fill the username input
await this.usernameInput.click();
await this.usernameInput.fill(username);
// Click the password wrapper div (as per codegen)
await this.passwordDivWrapper.click();
// Fill the password input
await this.passwordInput.click();
await this.passwordInput.fill(password);
// Click the login button
+276
View File
@@ -0,0 +1,276 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from 'playwright/pages/login.page';
/**
* Login Responsive Layout Tests
*
* This test suite validates that the login page displays correctly
* and fills the viewport at different screen sizes.
*/
const VIEWPORTS = {
mobile: { width: 375, height: 667 }, // iPhone SE
mobileLandscape: { width: 667, height: 375 },
tablet: { width: 768, height: 1024 }, // iPad
desktop: { width: 1280, height: 720 }, // Desktop
largeDesktop: { width: 1920, height: 1080 } // Full HD
};
test.describe('Login Page - Responsive Layout', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.navigate();
});
test('should fill viewport on mobile portrait (375x667)', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.mobile);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
// Check login wrapper fills viewport
const loginWrapper = loginPage.divLocator('.login-wrapper');
await expect(loginWrapper).toBeVisible();
const wrapperBox = await loginWrapper.boundingBox();
expect(wrapperBox?.height).toBeGreaterThanOrEqual(VIEWPORTS.mobile.height - 10);
// Check login panel exists and is visible
const loginPanel = loginPage.divLocator('.login-panel');
await expect(loginPanel).toBeVisible();
// Check all essential elements are visible
await expect(loginPage.divLocator('.logo-section')).toBeVisible();
await expect(loginPage.divLocator('.form-section')).toBeVisible();
await expect(loginPage.divLocator('.login-footer')).toBeVisible();
// Verify no horizontal scroll
const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
expect(bodyWidth).toBeLessThanOrEqual(VIEWPORTS.mobile.width + 1);
});
test('should fill viewport on tablet (768x1024)', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.tablet);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
const loginWrapper = loginPage.divLocator('.login-wrapper');
await expect(loginWrapper).toBeVisible();
const wrapperBox = await loginWrapper.boundingBox();
expect(wrapperBox?.height).toBeGreaterThanOrEqual(VIEWPORTS.tablet.height - 10);
// On tablet, hero panel should be hidden
const heroPanel = loginPage.divLocator('.hero-panel');
await expect(heroPanel).not.toBeVisible();
// Login panel should be visible
const loginPanel = loginPage.divLocator('.login-panel');
await expect(loginPanel).toBeVisible();
// All sections should be visible
await expect(loginPage.divLocator('.logo-section')).toBeVisible();
await expect(loginPage.divLocator('.form-section')).toBeVisible();
});
test('should fill viewport on desktop (1280x720)', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.desktop);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
const loginWrapper = loginPage.divLocator('.login-wrapper');
await expect(loginWrapper).toBeVisible();
const wrapperBox = await loginWrapper.boundingBox();
expect(wrapperBox?.height).toBeGreaterThanOrEqual(VIEWPORTS.desktop.height - 10);
// On desktop, hero panel should be visible
const heroPanel = loginPage.divLocator('.hero-panel');
await expect(heroPanel).toBeVisible();
// Login panel should be visible
const loginPanel = loginPage.divLocator('.login-panel');
await expect(loginPanel).toBeVisible();
// Login card should be centered
const loginCard = loginPage.divLocator('.login-card');
await expect(loginCard).toBeVisible();
});
test('should display all content without scrolling on desktop', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.desktop);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
// Check if login card content is fully visible
const loginCard = loginPage.divLocator('.login-card');
const cardBox = await loginCard.boundingBox();
// Logo should be visible
const logo = loginPage.divLocator('.logo-image');
const logoBox = await logo.boundingBox();
expect(logoBox).toBeTruthy();
// Username input should be visible
const usernameInput = page.locator('input[formcontrolname="username"]');
const usernameBox = await usernameInput.boundingBox();
expect(usernameBox).toBeTruthy();
// Password input should be visible
const passwordInput = page.locator('input[formcontrolname="password"]');
const passwordBox = await passwordInput.boundingBox();
expect(passwordBox).toBeTruthy();
// Login button should be visible
const loginButton = page.locator('mifosx-m3-button[type="submit"]');
const buttonBox = await loginButton.boundingBox();
expect(buttonBox).toBeTruthy();
// Resources section should be visible
const resourcesSection = page.locator('.resources-section');
const resourcesBox = await resourcesSection.boundingBox();
expect(resourcesBox).toBeTruthy();
// Footer should be visible
const footer = loginPage.divLocator('.login-footer');
const footerBox = await footer.boundingBox();
expect(footerBox).toBeTruthy();
});
test('should handle small mobile screens (375x667) with all content visible', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.mobile);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
// Scroll to bottom to ensure footer is accessible
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Footer should be visible after scroll
const footer = loginPage.divLocator('.login-footer');
await expect(footer).toBeVisible();
// Scroll back to top
await page.evaluate(() => window.scrollTo(0, 0));
// Logo and form should be visible at top
await expect(loginPage.divLocator('.logo-section')).toBeVisible();
await expect(loginPage.divLocator('.form-section')).toBeVisible();
});
test('should maintain proper spacing between elements on all viewports', async ({ page }) => {
const viewports = [
VIEWPORTS.mobile,
VIEWPORTS.tablet,
VIEWPORTS.desktop
];
for (const viewport of viewports) {
await page.setViewportSize(viewport);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
const logo = loginPage.divLocator('.logo-section');
const form = loginPage.divLocator('.form-section');
// All sections should be visible
await expect(logo).toBeVisible();
await expect(form).toBeVisible();
if (viewport.width > 1024) {
const resources = loginPage.divLocator('.resources-section');
await expect(resources).toBeVisible();
}
// Get bounding boxes
const logoBox = await logo.boundingBox();
const formBox = await form.boundingBox();
// Form should be below logo
if (logoBox && formBox) {
expect(formBox.y).toBeGreaterThan(logoBox.y + logoBox.height);
}
}
});
test('should not have horizontal overflow on any viewport', async ({ page }) => {
const viewports = [
VIEWPORTS.mobile,
VIEWPORTS.tablet,
VIEWPORTS.desktop
];
for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.goto('http://localhost:4200/login');
await page.waitForLoadState('networkidle');
// Check no horizontal scroll
const hasHorizontalScroll = await page.evaluate(() => {
return document.body.scrollWidth > window.innerWidth;
});
expect(hasHorizontalScroll).toBe(false);
}
});
test('should center login card vertically on desktop', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.desktop);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
const loginCard = page.locator('.login-card');
const cardBox = await loginCard.boundingBox();
const viewportHeight = VIEWPORTS.desktop.height;
// Card should be roughly centered (with some tolerance)
if (cardBox) {
const cardCenter = cardBox.y + cardBox.height / 2;
const viewportCenter = viewportHeight / 2;
// Allow 100px tolerance for header/footer
expect(Math.abs(cardCenter - viewportCenter)).toBeLessThan(150);
}
});
test('should show all form fields on mobile', async ({ page }) => {
await page.setViewportSize(VIEWPORTS.mobile);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
// Username field
const usernameField = page.locator('mat-form-field').filter({ hasText: 'Username' });
await expect(usernameField).toBeVisible();
// Password field
const passwordField = page.locator('mat-form-field').filter({ hasText: 'Password' });
await expect(passwordField).toBeVisible();
// Login button
const loginButton = page.locator('mifosx-m3-button[type="submit"]');
await expect(loginButton).toBeVisible();
});
test('should show logo on all screen sizes', async ({ page }) => {
const viewports = [
VIEWPORTS.mobile,
VIEWPORTS.tablet,
VIEWPORTS.desktop
];
for (const viewport of viewports) {
await page.setViewportSize(viewport);
// Verify the login form is visible
await loginPage.assertOnLoginPage();
const logo = page.locator('.logo-image');
await expect(logo).toBeVisible();
// Logo should have appropriate size
const logoBox = await logo.boundingBox();
expect(logoBox?.width).toBeGreaterThan(0);
expect(logoBox?.height).toBeGreaterThan(0);
}
});
});
@@ -0,0 +1,84 @@
/**
* Material Design 3 Toolbar Design Tokens
*
* This file contains all the design tokens for the toolbar component
* following Material Design 3 guidelines.
*/
:root {
// Surface Colors - Light Theme
--toolbar-surface: var(--md-sys-color-surface-container, #f3f3f3);
--toolbar-on-surface: var(--md-sys-color-on-surface, #1a1c1e);
--toolbar-on-surface-variant: var(--md-sys-color-on-surface-variant, #44474e);
--toolbar-primary: var(--md-sys-color-primary, #1074b9);
--toolbar-on-primary: var(--md-sys-color-on-primary, #fff);
--toolbar-primary-container: var(--md-sys-color-primary-container, #d1e4ff);
--toolbar-on-primary-container: var(--md-sys-color-on-primary-container, #001d35);
--toolbar-surface-container-high: var(--md-sys-color-surface-container-high, #e8e8eb);
// Dimensions
--toolbar-height: 64px;
--toolbar-height-mobile: 56px;
--toolbar-padding-inline: 16px;
--toolbar-padding-inline-mobile: 8px;
--toolbar-gap: 8px;
--toolbar-gap-small: 4px;
// Navigation Items
--toolbar-nav-item-padding: 12px 16px;
--toolbar-nav-item-padding-mobile: 8px 12px;
--toolbar-nav-item-border-radius: 20px;
--toolbar-nav-item-gap: 8px;
--toolbar-nav-item-min-height: 40px;
// Typography
--toolbar-font-family: 'Roboto', system-ui, sans-serif;
--toolbar-font-size: 14px;
--toolbar-font-weight: 500;
--toolbar-font-weight-active: 600;
--toolbar-line-height: 20px;
--toolbar-letter-spacing: 0.1px;
// Icon sizing
--toolbar-icon-size: 20px;
--toolbar-icon-button-size: 48px;
// State Layers (MD3 standard opacities)
--toolbar-state-hover-opacity: 0.08;
--toolbar-state-focus-opacity: 0.12;
--toolbar-state-pressed-opacity: 0.12;
--toolbar-state-active-opacity: 0.16;
// Elevation (MD3 Level 2)
--toolbar-elevation: 0px 2px 4px rgb(0 0 0 / 4%), 0px 1px 2px rgb(0 0 0 / 6%);
--toolbar-elevation-scrolled: 0px 4px 8px rgb(0 0 0 / 6%), 0px 2px 4px rgb(0 0 0 / 8%);
// Motion (MD3 easing curves)
--toolbar-easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
--toolbar-easing-emphasized: cubic-bezier(0.2, 0, 0, 1);
--toolbar-duration-short: 200ms;
--toolbar-duration-medium: 250ms;
// Search Component
--toolbar-search-background: var(--md-sys-color-surface-container-high, #e8e8eb);
--toolbar-search-border-radius: 28px;
--toolbar-search-padding: 4px 16px;
}
.dark-theme {
// Surface Colors - Dark Theme
--toolbar-surface: var(--md-sys-color-surface-container, #2c2f36);
--toolbar-on-surface: var(--md-sys-color-on-surface, #e2e2e5);
--toolbar-on-surface-variant: var(--md-sys-color-on-surface-variant, #c4c6d0);
--toolbar-primary: var(--md-sys-color-primary, #5ba2ec);
--toolbar-primary-container: var(--md-sys-color-primary-container, #003a6b);
--toolbar-on-primary-container: var(--md-sys-color-on-primary-container, #d1e4ff);
--toolbar-surface-container-high: var(--md-sys-color-surface-container-high, #3d4047);
// Elevation (MD3 Level 2 - Dark theme has stronger shadows)
--toolbar-elevation: 0px 2px 4px rgb(0 0 0 / 12%), 0px 1px 2px rgb(0 0 0 / 16%);
--toolbar-elevation-scrolled: 0px 4px 8px rgb(0 0 0 / 16%), 0px 2px 4px rgb(0 0 0 / 20%);
// Search Component
--toolbar-search-background: var(--md-sys-color-surface-container-high, #3d4047);
}
@@ -16,7 +16,7 @@ import {
import { MatDialog } from '@angular/material/dialog';
import { MatSidenav } from '@angular/material/sidenav';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Router, RouterLink } from '@angular/router';
import { Router } from '@angular/router';
/** rxjs Imports */
import { Observable, of } from 'rxjs';
@@ -31,7 +31,7 @@ import { ConfigurationWizardService } from '../../../configuration-wizard/config
import { ConfigurationWizardComponent } from '../../../configuration-wizard/configuration-wizard.component';
import { NotificationsTrayComponent } from 'app/shared/notifications-tray/notifications-tray.component';
import { MatToolbar } from '@angular/material/toolbar';
import { MatIconButton, MatButton } from '@angular/material/button';
import { MatIconButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
@@ -43,8 +43,6 @@ import { ThemeToggleComponent } from '../../../shared/theme-toggle/theme-toggle.
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { DocumentationLinksService } from 'app/shared/services/documentation-links.service';
import { environment } from '../../../../environments/environment';
/**
* Toolbar component.
*/
@@ -2,39 +2,47 @@
@if (!oauthEnabled) {
<div>
<form class="layout-column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
<mat-form-field class="login-input flex-align-center">
<span matPrefix class="m-r-10">
<fa-icon icon="user-circle" size="lg"></fa-icon>
</span>
<mat-form-field class="login-field flex-align-center">
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput type="text" autocomplete="off" formControlName="username" />
<input
matInput
type="text"
autocomplete="username"
formControlName="username"
[placeholder]="'labels.placeholders.Enter your username' | translate"
/>
<mifosx-m3-icon matIconPrefix name="account_circle"></mifosx-m3-icon>
@if (loginForm.controls.username.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Username' | translate }} <strong>{{ 'labels.commons.is required' | translate }}</strong>
{{ 'labels.inputs.Username' | translate }}
<strong>{{ 'labels.commons.is required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field class="login-input flex-align-center">
@if (!loginForm.controls.password.value) {
<span matPrefix class="m-r-10">
<fa-icon icon="lock" size="lg"></fa-icon>
</span>
}
<!-- Password Field -->
<mat-form-field class="login-field" appearance="fill">
<mat-label>{{ 'labels.inputs.Password' | translate }}</mat-label>
<input matInput type="{{ passwordInputType }}" formControlName="password" />
<input
matInput
[type]="passwordInputType"
autocomplete="current-password"
formControlName="password"
[placeholder]="'labels.placeholders.Enter your password' | translate"
/>
<mifosx-m3-icon matIconPrefix name="lock"></mifosx-m3-icon>
@if (loginForm.controls.password.value && !loading) {
<button
type="button"
matSuffix
mat-icon-button
(mousedown)="togglePasswordVisibility()"
(mouseup)="togglePasswordVisibility()"
(click)="togglePasswordVisibility()"
[attr.aria-label]="passwordInputType === 'password' ? 'Show password' : 'Hide password'"
>
@if (passwordInputType === 'password') {
<fa-icon icon="eye"></fa-icon>
}
@if (passwordInputType === 'text') {
<fa-icon icon="eye-slash"></fa-icon>
<mifosx-m3-icon name="visibility"></mifosx-m3-icon>
} @else {
<mifosx-m3-icon name="visibility_off"></mifosx-m3-icon>
}
</button>
}
@@ -45,20 +53,32 @@
</mat-error>
}
</mat-form-field>
<!-- Loading Progress -->
@if (loading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<mat-progress-bar mode="indeterminate" class="login-progress"></mat-progress-bar>
}
<button
mat-raised-button
color="primary"
class="login-button-first flex-align-center"
[disabled]="!loginForm.valid"
<!-- Submit Button -->
<mifosx-m3-button
variant="filled"
type="submit"
class="login-submit-button"
[disabled]="!loginForm.valid || loading"
[fullWidth]="true"
[label]="(loading ? 'labels.buttons.Signing in...' : 'labels.buttons.Login') | translate"
>
{{ 'labels.buttons.Login' | translate }}
@if (loading) {
<mat-spinner [diameter]="20"></mat-spinner>
<mat-spinner [diameter]="20" class="button-spinner"></mat-spinner>
}
</button>
</mifosx-m3-button>
<!-- Forgot Password Link -->
<div class="login-actions">
<a href="#" class="forgot-password-link">
{{ 'labels.links.Forgot Password?' | translate }}
</a>
</div>
</form>
</div>
}
@@ -1,37 +1,476 @@
/**
* Modern Material Design 3 Login Form Styles
*/
// ===== Main Form Container =====
.modern-login-form {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
#login-form {
.login-input {
width: 14rem;
margin-bottom: 0.1rem;
}
display: flex;
flex-direction: column;
gap: 0.25rem;
animation: slide-in 0.4s ease-out;
flex: 1;
}
[matPrefix] fa-icon {
font-size: 16px;
position: absolute;
top: 50%;
left: 190px;
transform: translateY(-50%);
}
// ===== Tenant Selector Styles =====
::ng-deep .tenant-section {
width: 100%;
.login-button-first {
width: 14rem;
margin-top: 0.8rem;
background-color: #1976d2;
color: white;
border-radius: 8px;
margin-bottom: 0.5rem;
}
mifosx-tenant-selector {
display: block;
width: 100%;
mat-spinner {
float: right;
margin: 0.5rem 0;
#tenant-selector {
width: 100%;
margin-bottom: 0;
// Apply same styling as login fields
.mat-mdc-text-field-wrapper {
border-radius: 12px 12px 0 0;
}
.mat-mdc-form-field-focus-overlay {
border-radius: 12px 12px 0 0;
background-color: var(--md-sys-color-on-surface, #1a1c1e);
opacity: 0.04;
}
&.mat-focused {
.mat-mdc-form-field-focus-overlay {
opacity: 0.12;
}
}
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-primary, #1074b9);
}
.mat-mdc-select {
font-size: 1rem;
font-weight: 400;
color: var(--md-sys-color-on-surface, #1a1c1e);
}
.mat-mdc-floating-label {
font-size: 1rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
.mat-mdc-form-field-icon-prefix {
padding-right: 0.75rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
fa-icon {
font-size: 1.25rem;
opacity: 0.7;
}
}
}
.tenantselector {
width: 100%;
}
}
}
::ng-deep .mat-mdc-form-field-infix {
width: 144px !important;
// ===== Form Fields =====
.login-field {
width: 100%;
margin-bottom: 0;
::ng-deep {
// Field container
.mat-mdc-text-field-wrapper {
border-radius: 12px 12px 0 0;
}
// Focus overlay
.mat-mdc-form-field-focus-overlay {
border-radius: 12px 12px 0 0;
background-color: var(--md-sys-color-on-surface, #1a1c1e);
opacity: 0.04;
}
// Focus state
&.mat-focused {
.mat-mdc-form-field-focus-overlay {
opacity: 0.12;
}
}
// Bottom indicator line
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-primary, #1074b9);
}
// Input field
.mat-mdc-input-element {
font-size: 1rem;
font-weight: 400;
color: var(--md-sys-color-on-surface, #1a1c1e);
padding: 0.875rem 0.75rem;
&::placeholder {
color: var(--md-sys-color-on-surface-variant, #44474e);
opacity: 0.6;
}
}
// Label
.mat-mdc-floating-label {
font-size: 1rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
// Prefix icon
.mat-mdc-form-field-icon-prefix {
padding-right: 0.75rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
fa-icon {
font-size: 1.25rem;
opacity: 0.7;
}
}
// Suffix button (password toggle)
.mat-mdc-form-field-icon-suffix {
button {
background: transparent;
border: none;
color: var(--md-sys-color-on-surface-variant, #44474e);
transition: all 0.2s ease;
&:hover {
color: var(--md-sys-color-primary, #1074b9);
background: #1074b914;
}
}
}
// Error state
&.mat-form-field-invalid {
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-error, #ba1a1a);
}
.mat-mdc-form-field-icon-prefix fa-icon {
color: var(--md-sys-color-error, #ba1a1a);
}
}
// Error message
.mat-mdc-form-field-error {
font-size: 0.75rem;
margin-top: 0.25rem;
color: var(--md-sys-color-error, #ba1a1a);
}
}
}
.container-margin {
margin-top: 8rem;
margin-bottom: 8rem;
// ===== Loading Progress =====
.login-progress {
height: 3px;
border-radius: 4px;
margin: -0.5rem 0 0.5rem;
::ng-deep {
.mdc-linear-progress__bar-inner {
border-color: var(--md-sys-color-primary, #1074b9);
}
}
}
// ===== Submit Button =====
.login-submit-button {
min-height: 48px;
color: var(--md-sys-color-on-surface-variant, #44474e);
margin-top: 0.5rem;
::ng-deep md-filled-button {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.01em;
}
.button-spinner {
margin-left: 0.75rem;
::ng-deep circle {
stroke: currentcolor;
}
}
}
// ===== Login Actions =====
.login-actions {
display: flex;
justify-content: center;
margin-top: 0.5rem;
}
.forgot-password-link {
font-size: 0.875rem;
color: var(--md-sys-color-primary, #1074b9);
text-decoration: none;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 20px;
transition: all 0.2s ease;
&:hover {
background: #1074b914;
text-decoration: underline;
}
&:focus {
outline: 2px solid var(--md-sys-color-primary, #1074b9);
outline-offset: 2px;
}
}
// ===== OIDC Login =====
.oidc-login-container {
width: 100%;
padding: 1rem 0;
}
.oidc-login-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
text-align: center;
}
.oidc-description {
font-size: 0.95rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
margin: 0;
line-height: 1.5;
}
.oidc-logged-in {
display: flex;
flex-direction: column;
gap: 2rem;
}
.welcome-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem;
background: var(--md-sys-color-primary-container, #d1e4ff);
border-radius: 16px;
.success-icon {
font-size: 3rem;
color: var(--md-sys-color-primary, #1074b9);
}
h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--md-sys-color-on-primary-container, #001d36);
}
}
.oidc-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
mifosx-m3-button {
flex: 1;
min-width: 140px;
}
}
.protected-data {
padding: 1rem;
background: var(--md-sys-color-surface-container, #f3f3f3);
border-radius: 12px;
border: 1px solid var(--md-sys-color-outline-variant, #c4c6d0);
max-height: 300px;
overflow: auto;
pre {
margin: 0;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
color: var(--md-sys-color-on-surface, #1a1c1e);
white-space: pre-wrap;
word-wrap: break-word;
}
}
// ===== Dark Theme Support =====
:host-context(.dark-theme) {
// Tenant selector dark theme
::ng-deep .tenant-section {
mifosx-tenant-selector {
#tenant-selector {
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-prefix {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
}
}
.login-field {
::ng-deep {
.mat-mdc-input-element {
color: var(--md-sys-color-on-surface, #e2e2e5);
&::placeholder {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-prefix {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-suffix button {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
&:hover {
color: var(--md-sys-color-primary, #5ba2ec);
background: #5ba2ec1f;
}
}
}
}
.forgot-password-link {
color: var(--md-sys-color-primary, #5ba2ec);
&:hover {
background: #5ba2ec1f;
}
}
.oidc-description {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.welcome-message {
background: var(--md-sys-color-primary-container, #003a6b);
.success-icon {
color: var(--md-sys-color-primary, #5ba2ec);
}
h3 {
color: var(--md-sys-color-on-primary-container, #d1e4ff);
}
}
.protected-data {
background: var(--md-sys-color-surface-container, #2c2f36);
border-color: var(--md-sys-color-outline-variant, #44474e);
pre {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
// ===== Responsive Design =====
@media (width <= 768px) {
#login-form {
gap: 0.25rem;
}
.login-field {
margin-bottom: 0;
}
.login-submit-button {
margin-top: 0.5rem;
}
.login-actions {
margin-top: 0.5rem;
}
}
@media (width <= 480px) {
#login-form {
gap: 0.15rem;
}
.login-field {
margin-bottom: 0;
::ng-deep {
.mat-mdc-input-element {
font-size: 0.95rem;
padding: 0.65rem 0.5rem;
}
.mat-mdc-floating-label {
font-size: 0.9rem;
}
.mat-mdc-form-field-icon-prefix {
padding-right: 0.5rem;
}
}
}
.login-submit-button {
min-height: 42px;
margin-top: 0.4rem;
}
.login-actions {
margin-top: 0.4rem;
}
.forgot-password-link {
font-size: 0.8125rem;
padding: 0.35rem 0.65rem;
}
.oidc-actions {
flex-direction: column;
mifosx-m3-button {
width: 100%;
}
}
}
// ===== Animations =====
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@@ -8,7 +8,8 @@ import { finalize } from 'rxjs/operators';
/** Custom Services */
import { AuthenticationService } from '../../core/authentication/authentication.service';
import { MatPrefix } from '@angular/material/form-field';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { M3IconComponent } from '../../shared/m3-ui/m3-icon/m3-icon.component';
import { M3ButtonComponent } from '../../shared/m3-ui/m3-button/m3-button.component';
import { MatProgressBar } from '@angular/material/progress-bar';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -25,7 +26,8 @@ import { environment } from '../../../environments/environment';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatPrefix,
FaIconComponent,
M3IconComponent,
M3ButtonComponent,
MatProgressBar,
MatProgressSpinner
]
+157 -126
View File
@@ -1,148 +1,179 @@
<!-- The viewport is divided into two parts having max-width 70% and 30% -->
<div class="layout-row">
<!--
Content in 70%: (TODO: Allow customization by organization)
Cover Image
Organization/Application Name
Organization/Application Description
This content will be hidden for smaller screens.
-->
<div class="flex-70 layout-column align-center align-items-center hide-lt-sm cover">
<div class="cover-content">
<h1 class="mat-display-1 cover-title">{{ 'APP_NAME' | translate }}</h1>
<p class="mat-headline cover-description">
{{ 'APP_NAME' | translate }} {{ 'labels.text.is designed by the' | translate }}
<a [href]="'mifosHome' | documentationLink" target="_blank" rel="noopener noreferrer" class="cover-link">{{
'labels.text.Mifos Initiative' | translate
}}</a
>. {{ 'labels.text.A' | translate }}
<a [href]="'mifosCommunity' | documentationLink" target="_blank" rel="noopener noreferrer" class="cover-link">{{
'labels.text.global community' | translate
}}</a>
{{ 'labels.text.elimination of poverty' | translate }}
<a [href]="'mifosVolunteer' | documentationLink" target="_blank" rel="noopener noreferrer" class="cover-link"
>{{ 'labels.text.Get involved' | translate }}!</a
>
</p>
<!-- Modern Material Design 3 Login Layout -->
<div class="login-wrapper">
<!-- Left Panel - Hero Section (Hidden on mobile) -->
<div class="hero-panel hide-lt-md">
<div class="hero-overlay"></div>
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title">{{ 'APP_NAME' | translate }}</h1>
<p class="hero-subtitle">
{{ 'labels.text.A' | translate }}
<span class="hero-highlight">{{ 'labels.text.global community' | translate }}</span>
{{ 'labels.text.elimination of poverty' | translate }}
</p>
<div class="hero-features">
<div class="feature-item">
<fa-icon icon="shield-alt" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Secure & Reliable' | translate }}</span>
</div>
<div class="feature-item">
<fa-icon icon="users" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Community Driven' | translate }}</span>
</div>
<div class="feature-item">
<fa-icon icon="globe" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Global Impact' | translate }}</span>
</div>
</div>
<div class="hero-links">
<a [href]="'mifosHome' | documentationLink" target="_blank" rel="noopener noreferrer" class="hero-link">
{{ 'labels.text.Mifos Initiative' | translate }}
<fa-icon icon="external-link-alt" size="sm"></fa-icon>
</a>
<a [href]="'mifosVolunteer' | documentationLink" target="_blank" rel="noopener noreferrer" class="hero-link">
{{ 'labels.text.Get involved' | translate }}
<fa-icon icon="external-link-alt" size="sm"></fa-icon>
</a>
</div>
</div>
<mifosx-footer variant="compact"></mifosx-footer>
</div>
</div>
<!--
Content in 30%: (TODO: Allow customization of Name, Logo and Contact Information by organization)
Language Selector
Organization/Application Name with Logo
Form
Contact Information/Resources
Footer
-->
<div class="layout-column flex-gt-sm-30 login-container">
<!-- Language Selector to the extreme right -->
<div class="layout-row-reverse">
<mifosx-language-selector class="p-t-10 p-r-10 width-40"></mifosx-language-selector>
<mifosx-theme-toggle class="p-t-10"></mifosx-theme-toggle>
<!-- Right Panel - Login Form -->
<div class="login-panel">
<!-- Header Controls -->
<div class="login-header">
@if (allowServerSwitch()) {
<mifosx-server-selector class="p-t-10"></mifosx-server-selector>
<mifosx-server-selector class="header-control" appearance="outline" [showLabel]="false">
</mifosx-server-selector>
}
<mifosx-theme-toggle class="header-control theme-toggle"></mifosx-theme-toggle>
<mifosx-language-selector class="header-control" appearance="outline" [showLabel]="false">
</mifosx-language-selector>
</div>
<!-- Logo with Organization Name -->
<div class="layout-row align-center-center flex-auto">
<img [src]="logoPath" (error)="onLogoError()" alt="{{ 'APP_NAME' | translate }} Logo" class="img-container" />
</div>
<!-- Main Login Card -->
<div class="login-card-container">
<div class="login-card">
<!-- Logo Section -->
<div class="logo-section">
@if (!themeDarkEnabled) {
<img [src]="logoPath" (error)="onLogoError()" alt="{{ 'APP_NAME' | translate }} Logo" class="logo-image" />
} @else {
<img src="assets/images/white-mifos.png" alt="{{ 'APP_NAME' | translate }} Logo" class="logo-image" />
}
<h2 class="welcome-text">{{ 'labels.heading.Welcome to Mifos' | translate }}</h2>
</div>
<!-- Form Section -->
<div class="layout-column align-center-center">
<!-- Tenant Identifier Name -->
@if (displayTenantSelector()) {
<mifosx-tenant-selector></mifosx-tenant-selector>
}
<!-- Form Section -->
<div class="form-section">
<!-- Tenant Selector -->
@if (displayTenantSelector()) {
<div class="tenant-section">
<mifosx-tenant-selector></mifosx-tenant-selector>
</div>
}
<!--
Type of Form:
Login
Reset Password
Two Factor Authentication
-->
@if (!resetPassword && !twoFactorAuthenticationRequired) {
<mifosx-login-form></mifosx-login-form>
}
<!-- Login Forms -->
@if (!resetPassword && !twoFactorAuthenticationRequired) {
<mifosx-login-form></mifosx-login-form>
}
<div class="layout-row">
@if (resetPassword) {
<mifosx-reset-password></mifosx-reset-password>
}
@if (twoFactorAuthenticationRequired) {
<mifosx-two-factor-authentication></mifosx-two-factor-authentication>
}
@if (resetPassword) {
<mifosx-reset-password></mifosx-reset-password>
}
@if (twoFactorAuthenticationRequired) {
<mifosx-two-factor-authentication></mifosx-two-factor-authentication>
}
</div>
<!-- Resources Links -->
<div class="resources-section">
<div class="resources-divider">
<span class="divider-text">{{ 'labels.links.Resources' | translate }}</span>
</div>
<div class="resources-links">
<button mat-button [matMenuTriggerFor]="resourcesMenu" class="resource-button">
<fa-icon icon="book" size="sm"></fa-icon>
{{ 'labels.links.Resources' | translate }}
</button>
<button mat-button [matMenuTriggerFor]="communityMenu" class="resource-button">
<fa-icon icon="users" size="sm"></fa-icon>
{{ 'labels.links.Community' | translate }}
</button>
<button mat-button [matMenuTriggerFor]="contributeMenu" class="resource-button">
<fa-icon icon="heart" size="sm"></fa-icon>
{{ 'labels.links.Contribute' | translate }}
</button>
</div>
</div>
</div>
</div>
<div class="layout-row">
<!-- Contact Information/Resources (hidden on smaller screens) -->
<mat-list class="information-list align-items-center custom-horizontal-list hide-lt-lg">
<mat-list-item
><button mat-button [matMenuTriggerFor]="resourcesMenu">
{{ 'labels.links.Resources' | translate }}
</button></mat-list-item
<!-- Footer -->
<div class="login-footer">
<div class="layout-column copy-label">
<p class="footer-content align-center">
{{ 'APP_NAME' | translate }} {{ 'labels.text.by' | translate }} Mifos
</p>
</div>
<button
mat-icon-button
(click)="reloadSettings()"
class="reload-button"
matTooltip="Reload Settings"
attr.aria-label="Reload Settings"
>
<mat-list-item
><button mat-button [matMenuTriggerFor]="communityMenu">
{{ 'labels.links.Community' | translate }}
</button></mat-list-item
>
<mat-list-item
><button mat-button [matMenuTriggerFor]="contributeMenu">
{{ 'labels.links.Contribute' | translate }}
</button></mat-list-item
>
</mat-list>
</div>
<!-- Footer -->
<mifosx-footer [styleClass]="'m-t-90'"></mifosx-footer>
<div class="layout-row m-b-20 m-r-10" align="end">
<a (click)="reloadSettings()"><fa-icon icon="refresh"></fa-icon></a>
<mifosx-m3-icon name="refresh"></mifosx-m3-icon>
</button>
</div>
</div>
</div>
</div>
<!-- Contact Information/Resources Menus -->
<mat-menu #resourcesMenu="matMenu">
<a [href]="'userManual' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.User Manual' | translate
}}</a>
<a [href]="'fineractFunctionalSpecs' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Functional Specifications' | translate
}}</a>
<a [href]="'fineractContributorZone' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Developer Zone' | translate
}}</a>
<!-- Resource Menus -->
<mat-menu #resourcesMenu="matMenu" class="modern-menu">
<a [href]="'userManual' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="book-open" class="menu-icon"></fa-icon>
{{ 'labels.links.User Manual' | translate }}
</a>
<a [href]="'fineractFunctionalSpecs' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="file-alt" class="menu-icon"></fa-icon>
{{ 'labels.links.Functional Specifications' | translate }}
</a>
<a [href]="'fineractContributorZone' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="code" class="menu-icon"></fa-icon>
{{ 'labels.links.Developer Zone' | translate }}
</a>
</mat-menu>
<mat-menu #communityMenu="matMenu">
<a [href]="'mifosUserGroup' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.User Group' | translate
}}</a>
<a [href]="'mifosDeveloperGroup' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Developer Group' | translate
}}</a>
<a [href]="'mifosIrc' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.IRC' | translate
}}</a>
<mat-menu #communityMenu="matMenu" class="modern-menu">
<a [href]="'mifosUserGroup' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="users" class="menu-icon"></fa-icon>
{{ 'labels.links.User Group' | translate }}
</a>
<a [href]="'mifosDeveloperGroup' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="laptop-code" class="menu-icon"></fa-icon>
{{ 'labels.links.Developer Group' | translate }}
</a>
<a [href]="'mifosIrc' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="comments" class="menu-icon"></fa-icon>
{{ 'labels.links.IRC' | translate }}
</a>
</mat-menu>
<mat-menu #contributeMenu="matMenu">
<a [href]="'keyDesignPrinciples' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Key Design Principles' | translate
}}</a>
<a [href]="'sourceforgeMifos' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Working with Code' | translate
}}</a>
<a [href]="'mifosDonate' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>{{
'labels.links.Donate' | translate
}}</a>
<mat-menu #contributeMenu="matMenu" class="modern-menu">
<a [href]="'keyDesignPrinciples' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="lightbulb" class="menu-icon"></fa-icon>
{{ 'labels.links.Key Design Principles' | translate }}
</a>
<a [href]="'sourceforgeMifos' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="code-branch" class="menu-icon"></fa-icon>
{{ 'labels.links.Working with Code' | translate }}
</a>
<a [href]="'mifosDonate' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="hand-holding-heart" class="menu-icon"></fa-icon>
{{ 'labels.links.Donate' | translate }}
</a>
</mat-menu>
+749 -102
View File
@@ -1,140 +1,787 @@
mifosx-server-selector {
padding-left: 5px;
width: 40%;
/**
* Modern Material Design 3 Login Component Styles
*/
// ===== Main Wrapper =====
.login-wrapper {
display: flex;
min-height: 100vh;
height: 100%;
width: 100%;
background: var(--md-sys-color-background, #fafafa);
}
.mat-mdc-form-field-infix {
width: 144px !important;
}
mifosx-tenant-selector {
border-radius: 8px;
width: 14rem;
margin-bottom: 0.1rem;
::ng-deep .mat-mdc-form-field {
width: 14rem;
}
::ng-deep .mat-mdc-form-field-infix {
width: 144px !important;
border-radius: 8px;
}
::ng-deep [matPrefix] fa-icon {
font-size: 16px;
position: absolute;
left: 190px;
border-radius: 8px;
top: 50%;
transform: translateY(-50%);
}
}
.cover {
// ===== Hero Panel (Left Side) =====
.hero-panel {
flex: 0 0 70%;
position: relative;
background: url('../../assets/images/cover_image_resized.webp') no-repeat center center;
background-size: cover;
position: relative;
z-index: 0;
max-width: 70vw;
overflow: hidden;
&::after {
content: '';
opacity: 0.5;
background: linear-gradient(#000, #000);
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
z-index: 1;
// Dark overlay for better text contrast
.hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #1074b99e 0%, #004989a6 100%);
z-index: 1;
}
.width-40 {
width: 40%;
}
.cover-content {
display: block;
max-width: 80%;
margin: 100px;
color: #fff;
text-align: justify;
.hero-content {
position: relative;
z-index: 2;
font-size: 1.2rem;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 4rem;
}
.cover-title {
border-bottom: 2px solid #fff;
margin: 0 0 20px;
padding-bottom: 10px;
.hero-text {
max-width: 600px;
color: #fff;
animation: fade-in-up 0.8s ease-out;
flex: 0 1 auto;
}
// Footer in hero panel
mifosx-footer {
width: 100%;
margin-top: auto;
padding-top: 2rem;
::ng-deep .footer-compact {
color: rgb(255 255 255 / 90%);
align-items: center;
text-align: center;
.footer-info {
justify-content: center;
.info-label {
color: rgb(255 255 255 / 80%);
font-weight: 500;
}
.info-value {
color: rgb(255 255 255 / 95%);
font-weight: 400;
}
.info-separator {
color: rgb(255 255 255 / 50%);
}
}
.footer-copyright {
color: rgb(255 255 255 / 75%);
font-size: 0.875rem;
text-align: center;
}
}
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
margin: 0 0 1.5rem;
line-height: 1.2;
letter-spacing: -0.02em;
background: linear-gradient(135deg, #fff 0%, #e3f2fd 100%);
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.25rem;
line-height: 1.6;
margin: 0 0 3rem;
opacity: 0.95;
font-weight: 400;
}
.hero-highlight {
font-weight: 600;
color: #b4d575;
}
.hero-features {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 3rem;
}
.feature-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #ffffff1a;
border-radius: 12px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
&:hover {
background: #ffffff26;
transform: translateX(8px);
}
}
.feature-icon {
font-size: 1.5rem;
color: #b4d575;
}
.feature-text {
font-size: 1rem;
font-weight: 500;
}
.hero-links {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.hero-link {
color: #fff;
text-decoration: none;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: 2px solid #ffffff4d;
border-radius: 24px;
transition: all 0.3s ease;
&:hover {
background: #ffffff1a;
border-color: #fff;
transform: translateY(-2px);
}
.cover-description {
.cover-link {
text-decoration: underline;
color: #fff;
fa-icon {
opacity: 0.7;
}
}
}
&:hover {
color: #d3d3d3;
// ===== Login Panel (Right Side) =====
.login-panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--md-sys-color-surface, #fff);
position: relative;
min-height: 100vh;
overflow-y: auto;
}
.login-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 1.5rem;
background: transparent;
.header-control {
opacity: 0.8;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
}
.theme-toggle {
min-height: -webkit-fill-available;
}
// Theme Toggle Button Styles
::ng-deep mifosx-theme-toggle {
button {
color: var(--md-sys-color-on-surface, #1a1c1e);
transition: all 0.3s ease;
&:hover {
background: #1074b914;
color: var(--md-sys-color-primary, #1074b9);
}
mat-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
}
}
// Language Selector Outlined Styles
::ng-deep mifosx-language-selector {
width: 20%;
#language-selector.outlined-variant {
width: 100%;
margin: 0;
// Compact outlined field for header
.mat-mdc-form-field-flex {
height: 44px;
}
.mat-mdc-form-field-infix {
padding: 10px 0;
min-height: 44px;
}
// Rounded corners for Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading {
border-radius: 12px 0 0 12px;
width: 12px;
}
.mdc-notched-outline__trailing {
border-radius: 0 12px 12px 0;
}
}
}
}
// Server Selector Outlined Styles
::ng-deep mifosx-server-selector {
width: 70%;
#server-selector.outlined-variant {
width: 100%;
margin: 0;
// Compact outlined field for header
.mat-mdc-form-field-flex {
height: 44px;
}
.mat-mdc-form-field-infix {
padding: 10px 0;
min-height: 44px;
}
// Rounded corners for Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading {
border-radius: 12px 0 0 12px;
width: 12px;
}
.mdc-notched-outline__trailing {
border-radius: 0 12px 12px 0;
}
}
}
}
}
.login-container {
box-shadow: 1px 1px 50px #000;
height: 100vh;
width: 100%;
min-width: 21.5rem;
padding: 0.5rem 1rem;
.img-container {
max-width: 18rem;
margin-bottom: 2rem;
gap: 20px;
}
}
.layout-column {
// ===== Login Card Container =====
.login-card-container {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
height: 100%;
animation: fade-in 0.6s ease-out;
}
.custom-horizontal-list {
display: flex !important;
flex-direction: row !important;
.login-card {
width: 100%;
max-width: 480px;
background: var(--md-sys-color-surface-container-low, #fff);
border-radius: 28px;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
&:hover {
box-shadow:
0 2px 4px #00000014,
0 4px 12px #0000000f,
0 12px 32px #0000001f;
}
}
// ===== Logo Section =====
.logo-section {
text-align: center;
margin-bottom: 1rem;
flex-shrink: 0;
animation: fade-in-down 0.6s ease-out 0.2s both;
}
.logo-image {
max-width: 180px;
height: auto;
margin-bottom: 1rem;
filter: drop-shadow(0 2px 8px #0000001a);
}
.welcome-text {
font-size: 1.75rem;
font-weight: 600;
margin: 0 0 0.5rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
letter-spacing: -0.01em;
}
.welcome-subtitle {
font-size: 0.95rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
margin: 0;
font-weight: 400;
}
// ===== Form Section =====
.form-section {
margin-bottom: 1rem;
flex: 0 1 auto;
display: flex;
flex-direction: column;
animation: fade-in-up 0.6s ease-out 0.3s both;
}
.tenant-section {
margin-bottom: 1rem;
flex-shrink: 0;
}
// ===== Resources Section =====
.resources-section {
margin-top: auto;
padding-top: 1rem;
flex-shrink: 0;
animation: fade-in-up 0.6s ease-out 0.4s both;
}
.resources-divider {
position: relative;
text-align: center;
margin-bottom: 1.5rem;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: var(--md-sys-color-outline-variant, #c4c6d0);
}
.divider-text {
position: relative;
display: inline-block;
padding: 0 1rem;
background: var(--md-sys-color-surface-container-low, #fff);
color: var(--md-sys-color-on-surface-variant, #44474e);
font-size: 0.85rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
.resources-links {
display: flex;
justify-content: center;
align-items: center;
gap: 1.5rem;
gap: 0.5rem;
flex-wrap: wrap;
}
mat-list-item {
width: auto !important;
min-width: 0 !important;
padding: 0 !important;
.resource-button {
font-size: 0.875rem;
color: var(--md-sys-color-primary, #1074b9);
transition: all 0.2s ease;
[mat-button] {
white-space: nowrap;
fa-icon {
margin-right: 0.4rem;
opacity: 0.7;
}
&:hover {
background: #1074b914;
fa-icon {
opacity: 1;
}
}
}
.p-t-10 {
padding-top: 0;
// ===== Footer =====
.login-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.5rem;
position: relative;
margin-top: auto;
flex-direction: column;
flex-shrink: 0;
mifosx-footer {
flex: 1;
text-align: left;
}
.reload-button {
color: var(--mat-app-text-color, #c4c6d0);
opacity: 0.6;
transition: all 0.2s ease;
background: transparent;
border: none;
flex-shrink: 0;
&:hover {
opacity: 1;
transform: rotate(90deg);
}
}
}
.p-r-10 {
padding-right: 10px;
// ===== Modern Menus =====
::ng-deep .modern-menu {
border-radius: 12px;
box-shadow:
0 4px 12px #0000001a,
0 2px 4px #0000000f;
.mat-mdc-menu-item {
border-radius: 8px;
margin: 4px 8px;
transition: all 0.2s ease;
.menu-icon {
margin-right: 0.75rem;
color: var(--md-sys-color-primary, #1074b9);
opacity: 0.7;
}
&:hover .menu-icon {
opacity: 1;
}
}
}
::ng-deep .login-container mifosx-footer .divider {
margin: 0 !important;
width: 14rem !important;
margin-top: 2px !important;
// ===== Animations =====
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
::ng-deep .login-container #footer {
align-items: unset !important;
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in-down {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// ===== Dark Theme Support =====
:host-context(.dark-theme) {
.login-wrapper {
background: #1a1c1e;
}
.login-panel {
background: #1a1c1e;
}
.login-card {
background: #1a1c1e;
box-shadow:
0 1px 2px #0000004d,
0 2px 6px #0003,
0 8px 24px #0000004d;
}
.logo-image {
max-width: 20rem;
padding: 0;
margin: 0;
height: 63px;
content: url('../../assets/images/white-mifos.png');
}
.welcome-text {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.welcome-subtitle {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.resources-divider {
&::before {
background: var(--md-sys-color-outline-variant, #44474e);
}
.divider-text {
background: var(--md-sys-color-surface-container-low, #2c2f36);
color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
.resource-button {
color: var(--md-sys-color-primary, #5ba2ec);
&:hover {
background: #5ba2ec1f;
}
}
// Header controls dark theme
.login-header {
::ng-deep mifosx-theme-toggle {
button {
color: var(--md-sys-color-on-surface, #e2e2e5);
&:hover {
background: #5ba2ec1f;
color: var(--md-sys-color-primary, #5ba2ec);
}
}
}
// Language selector and server selector dark theme styles are handled by the components themselves
}
}
// ===== Responsive Design =====
// Tablet
@media (width <= 1024px) {
.hero-panel {
flex: 0 0 45%;
.hero-title {
font-size: 2.75rem;
}
.hero-subtitle {
font-size: 1.1rem;
}
.hero-content {
padding: 3rem 2rem;
}
mifosx-footer {
padding-top: 1.5rem;
::ng-deep .footer-compact {
.footer-info {
font-size: 0.7rem;
}
.footer-copyright {
font-size: 0.8rem;
}
}
}
}
.login-card {
padding: 1.5rem 1rem;
}
// Hide resources section on medium screens
.resources-section {
display: none;
}
}
// Mobile
@media (width <= 768px) {
.login-wrapper {
flex-direction: column;
}
.hero-panel {
display: none;
}
.login-panel {
min-height: 100vh;
}
.login-card-container {
padding: 1rem;
justify-content: center;
height: auto;
min-height: calc(100vh - 56px); // 56px for header
}
.login-header {
padding: 0.75rem 1rem;
gap: 0.25rem;
::ng-deep mifosx-language-selector {
width: 30%;
#language-selector.outlined-variant {
width: 100%;
font-size: 0.8rem;
}
}
::ng-deep mifosx-server-selector {
width: 60%;
#server-selector.outlined-variant {
width: 100%;
font-size: 0.8rem;
}
}
::ng-deep mifosx-theme-toggle {
button {
padding: 8px;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
}
.login-card {
padding: 0.75rem 1rem;
border-radius: 24px;
margin: 0;
}
.logo-section {
margin-bottom: 1rem;
}
.logo-image {
max-width: 140px;
margin-bottom: 0.75rem;
}
.welcome-text {
font-size: 1.5rem;
}
// Hide resources section on medium and small screens
.resources-section {
display: none;
}
.form-section {
margin-bottom: 0.75rem;
}
.login-footer {
flex-direction: column;
align-items: center;
padding: 0.5rem;
mifosx-footer {
text-align: center;
}
.reload-button {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
border: none;
margin-top: 0.5rem;
}
}
}
// Small mobile
@media (width <= 480px) {
.login-header {
padding: 0.5rem 0.75rem;
gap: 0.25rem;
}
.login-card-container {
padding: 0.5rem;
justify-content: center;
}
.login-card {
padding: 0.75rem 0.875rem;
}
.logo-section {
margin-bottom: 0.75rem;
}
.logo-image {
max-width: 110px;
margin-bottom: 0.5rem;
}
.welcome-text {
font-size: 1.25rem;
}
.welcome-subtitle {
font-size: 0.875rem;
}
.form-section {
margin-bottom: 0.5rem;
}
// Hide resources section on small screens
.resources-section {
display: none;
}
.login-footer {
padding: 0.25rem 1rem;
}
}
// ===== Utilities =====
.hide-lt-md {
@media (width <= 768px) {
display: none !important;
}
}
.hide-lt-lg {
@media (width <= 1024px) {
display: none !important;
}
}
+27 -7
View File
@@ -10,6 +10,7 @@ import { Alert } from '../core/alert/alert.model';
/** Custom Services */
import { AlertService } from '../core/alert/alert.service';
import { ThemingService } from '../shared/theme-toggle/theming.service';
/** Environment Imports */
import { environment } from '../../environments/environment';
@@ -21,11 +22,11 @@ import { TenantSelectorComponent } from '../shared/tenant-selector/tenant-select
import { LoginFormComponent } from './login-form/login-form.component';
import { ResetPasswordComponent } from './reset-password/reset-password.component';
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
import { MatList, MatListItem } from '@angular/material/list';
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
import { FooterComponent } from '../shared/footer/footer.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { M3IconComponent } from '../shared/m3-ui/m3-icon/m3-icon.component';
/**
* Login component.
@@ -43,18 +44,18 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
LoginFormComponent,
ResetPasswordComponent,
TwoFactorAuthenticationComponent,
MatList,
MatListItem,
MatMenuTrigger,
FooterComponent,
FaIconComponent,
MatMenu,
MatMenuItem
MatMenuItem,
M3IconComponent
]
})
export class LoginComponent implements OnInit, OnDestroy {
private alertService = inject(AlertService);
private settingsService = inject(SettingsService);
private themingService = inject(ThemingService);
private router = inject(Router);
public environment = environment;
@@ -66,12 +67,26 @@ export class LoginComponent implements OnInit, OnDestroy {
/** Subscription to alerts. */
alert$: Subscription;
logoPath = 'assets/images/default_home.png';
/** Subscription to theme changes. */
theme$: Subscription;
themeDarkEnabled: boolean = false;
/**
* Subscribes to alert event of alert service.
* Subscribes to alert event of alert service and theme changes.
*/
ngOnInit() {
this.updateLogo();
this.themeDarkEnabled = this.settingsService.themeDarkEnabled;
// Subscribe to theme changes
this.theme$ = this.themingService.theme.subscribe((value: string) => {
this.themeDarkEnabled = this.settingsService.themeDarkEnabled;
});
// Initialize theme based on settings
this.themingService.setDarkMode(!!this.settingsService.themeDarkEnabled);
// Subscribe to alerts
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => {
const alertType = alertEvent.type;
if (alertType === 'Password Expired') {
@@ -91,10 +106,15 @@ export class LoginComponent implements OnInit, OnDestroy {
}
/**
* Unsubscribes from alerts.
* Unsubscribes from alerts and theme changes.
*/
ngOnDestroy() {
this.alert$.unsubscribe();
if (this.alert$) {
this.alert$.unsubscribe();
}
if (this.theme$) {
this.theme$.unsubscribe();
}
}
reloadSettings(): void {
@@ -19,8 +19,7 @@
>
@if (passwordInputType === 'password') {
<fa-icon icon="eye"></fa-icon>
}
@if (passwordInputType === 'text') {
} @else {
<fa-icon icon="eye-slash"></fa-icon>
}
</button>
+58 -40
View File
@@ -1,41 +1,59 @@
<div class="layout-column m-b-20 f12" ngClass="{{ styleClass }}" id="footer" *ngIf="displayBackEndInfo">
<mat-divider class="divider align-center"></mat-divider>
<div class="layout-column m-b-20 content-wrapper footer-center">
<table class="versions">
<tr>
<td class="footer-content">Tenant</td>
<td class="right footer-content">
{{ tenant | titlecase }}
</td>
</tr>
<tr>
<td class="footer-content">Mifos</td>
<td class="right footer-content">
{{ versions.mifos }} - <b>{{ hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.text.Fineract' | translate }}</td>
<td class="right footer-content">
{{ versions.fineract.version }} - <b>{{ versions.fineract.hash }}</b>
</td>
</tr>
<tr *ngIf="displayBackEndInfo">
<td class="footer-content">{{ 'labels.inputs.Server' | translate }}</td>
<td class="right footer-content">
{{ server }}
</td>
</tr>
<tr *ngIf="isBusinessDateDefined">
<td class="footer-content">{{ 'labels.text.Current Business Date' | translate }}:</td>
<td class="center footer-content business-date">
<b>{{ businessDate | date: 'EEEE, MMMM dd, y' }}</b>
</td>
</tr>
</table>
@if (variant === 'compact') {
<!-- Compact mode for Login -->
<div class="footer-compact">
@if (displayBackEndInfo) {
<div class="footer-info">
<span class="info-item">
<span class="info-label">Mifos:</span>
<span class="info-value">{{ versions.mifos }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-item">
<span class="info-label">{{ 'labels.text.Fineract' | translate }}:</span>
<span class="info-value">{{ versions.fineract.version }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-item">
<span class="info-label">{{ 'labels.inputs.Server' | translate }}:</span>
<span class="info-value">{{ server }}</span>
</span>
</div>
}
</div>
<div class="layout-column m-b-20 copy-label">
<p class="footer-content align-center">{{ 'APP_NAME' | translate }} {{ 'labels.text.by' | translate }} Mifos</p>
</div>
</div>
} @else {
<!-- Default mode for Shell -->
@if (displayBackEndInfo) {
<div class="layout-column m-b-20 f12" ngClass="{{ styleClass }}" id="footer">
<div class="layout-column m-t-10 m-b-10 content-wrapper footer-center">
<table class="versions">
<tr>
<td class="footer-content">Mifos</td>
<td class="right footer-content">
{{ versions.mifos }} - <b>{{ hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.text.Fineract' | translate }}</td>
<td class="right footer-content">
{{ versions.fineract.version }} - <b>{{ versions.fineract.hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.inputs.Server' | translate }}</td>
<td class="right footer-content">
{{ server }}
</td>
</tr>
@if (isBusinessDateDefined) {
<tr>
<td class="footer-content">{{ 'labels.text.Current Business Date' | translate }}:</td>
<td class="center footer-content business-date">
<b>{{ businessDate | date: 'EEEE, MMMM dd, y' }}</b>
</td>
</tr>
}
</table>
</div>
</div>
}
}
+69 -1
View File
@@ -1,6 +1,58 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
// ===== Compact Mode (Login) =====
.footer-compact {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
width: 100%;
font-size: 0.75rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
.footer-info {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
line-height: 1.4;
}
.info-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.info-label {
font-weight: 500;
opacity: 0.8;
}
.info-value {
font-weight: 400;
opacity: 0.9;
}
.info-separator {
opacity: 0.4;
font-size: 0.875rem;
}
.footer-copyright {
font-size: 0.875rem;
opacity: 0.7;
margin-top: 0.25rem;
}
}
// Dark theme support for compact mode
.dark-theme .footer-compact {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
// ===== Default Mode (Shell) =====
#footer {
align-items: center;
min-width: 400px;
@@ -48,5 +100,21 @@
.copy-label {
min-width: 100%;
text-align: center;
font-size: large;
font-size: medium;
margin-bottom: 10px;
}
// ===== Responsive for Compact Mode =====
@media (width <= 480px) {
.footer-compact {
font-size: 0.7rem;
.footer-info {
gap: 0.375rem;
}
.footer-copyright {
font-size: 0.8rem;
}
}
}
+1 -2
View File
@@ -12,7 +12,6 @@ import { VersionService } from 'app/system/version.service';
import { environment } from '../../../environments/environment';
import { Subscription } from 'rxjs';
import { NgClass, DatePipe } from '@angular/common';
import { MatDivider } from '@angular/material/divider';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -25,7 +24,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
NgClass,
MatDivider,
DatePipe
]
})
@@ -38,6 +36,7 @@ export class FooterComponent implements OnInit, OnDestroy {
private versionService = inject(VersionService);
@Input() styleClass: string = '';
@Input() variant: 'default' | 'compact' = 'default';
/** Mifos X version. */
versions: any = {
+23 -1
View File
@@ -43,6 +43,7 @@ import {
faFileUpload,
faFileWord,
faFillDrip,
faGlobe,
faHandHoldingUsd,
faHome,
faKey,
@@ -113,7 +114,17 @@ import {
faPause,
faReceipt,
faTableCells,
faSave
faSave,
faExternalLinkAlt,
faHeart,
faBookOpen,
faCode,
faLaptopCode,
faComments,
faLightbulb,
faCodeBranch,
faHandHoldingHand,
faHandHoldingHeart
} from '@fortawesome/free-solid-svg-icons';
/**
@@ -137,6 +148,7 @@ export class IconsModule {
faBars,
faBell,
faBook,
faBookOpen,
faBriefcase,
faBuilding,
faCalculator,
@@ -150,9 +162,12 @@ export class IconsModule {
faChevronLeft,
faChevronRight,
faCircle,
faComments,
faHeartBroken,
faClock,
faCloud,
faCode,
faCodeBranch,
faCog,
faCogs,
faCommentAlt,
@@ -163,6 +178,7 @@ export class IconsModule {
faExchange,
faExclamationCircle,
faExclamationTriangle,
faExternalLinkAlt,
faEye,
faEyeSlash,
faFile,
@@ -173,9 +189,15 @@ export class IconsModule {
faFileExcel,
faFileExport,
faFillDrip,
faGlobe,
faHandHoldingHand,
faHandHoldingHeart,
faHandHoldingUsd,
faHeart,
faHome,
faKey,
faLaptopCode,
faLightbulb,
faLink,
faList,
faListUl,
@@ -1,6 +1,13 @@
<mat-form-field id="language-selector" class="m-l-10">
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
<mat-select [formControl]="languageSelector" (selectionChange)="setLanguage()" class="languageselector">
<mat-form-field id="language-selector" [appearance]="appearance" [class.outlined-variant]="appearance === 'outline'">
@if (showLabel) {
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
}
<mat-select
[formControl]="languageSelector"
(selectionChange)="setLanguage()"
class="languageselector"
[placeholder]="!showLabel ? ('labels.inputs.Language' | translate) : ''"
>
@for (language of languages; track language) {
<mat-option [value]="language">
{{ 'languages.' + language | translate }}
@@ -1,19 +1,131 @@
::ng-deep .mat-mdc-form-field-bottom-align::before {
border-bottom-color: white;
// ===== Base Language Selector Styles =====
#language-selector {
&.mat-mdc-form-field {
// Default styles (fill variant)
&:not(.outlined-variant) {
margin-left: 10px;
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
}
}
// Outlined variant styles (used in login)
&.outlined-variant {
.mat-mdc-text-field-wrapper {
padding: 0;
}
.mat-mdc-form-field-flex {
align-items: center;
}
.mat-mdc-form-field-infix {
padding: 12px 0;
min-height: 48px;
}
// Outlined border styles with Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #79747e);
border-width: 1px;
}
}
// Hover state
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #1a1c1e);
}
}
}
// Focused state
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #1074b9);
border-width: 2px;
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #1074b9);
}
}
// Select styling
.mat-mdc-select {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #44474e);
}
// Label styling
.mat-mdc-floating-label {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
}
}
}
::ng-deep .mdc-line-ripple::before {
border-bottom-color: white;
}
// Dark theme support for outlined variant
.dark-theme {
#language-selector.outlined-variant {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #938f99);
}
}
::ng-deep .mdc-line-ripple::after {
border-bottom-color: white;
}
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
::ng-deep .mat-mdc-form-field .mdc-line-ripple::before {
border-bottom-color: white !important;
}
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #5ba2ec);
}
}
::ng-deep .mat-mdc-form-field .mdc-line-ripple::after {
border-bottom-color: white !important;
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
}
@@ -1,5 +1,5 @@
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { Component, inject, Input } from '@angular/core';
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
/** Custom Services */
@@ -25,6 +25,12 @@ export class LanguageSelectorComponent {
private translateService = inject(TranslateService);
private settingsService = inject(SettingsService);
/** Appearance of the form field (fill, outline). Defaults to 'fill'. */
@Input() appearance: 'fill' | 'outline' = 'fill';
/** Show label in the form field. Defaults to true. */
@Input() showLabel: boolean = true;
/** Language selector form control. */
languageSelector = new UntypedFormControl();
+8
View File
@@ -0,0 +1,8 @@
/**
* Material Design 3 UI Components
*
* This module exports all Material Design 3 components for easy importing.
*/
export { M3ButtonComponent, M3ButtonVariant } from './m3-button/m3-button.component';
export { M3IconComponent, M3IconStyle } from './m3-icon/m3-icon.component';
@@ -0,0 +1,31 @@
<div [class]="containerClasses">
@if (variant === 'filled') {
<md-filled-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-filled-button>
}
@if (variant === 'outlined') {
<md-outlined-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-outlined-button>
}
@if (variant === 'elevated') {
<md-elevated-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-elevated-button>
}
@if (variant === 'text') {
<md-text-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-text-button>
}
@if (variant === 'tonal') {
<md-filled-tonal-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-filled-tonal-button>
}
</div>
@@ -0,0 +1,20 @@
// Material Web Button Component Styles
// Base styles are applied globally in src/theme/_material-web.scss
.m3-button-container {
display: inline-block;
&--full-width {
display: block;
width: 100%;
md-filled-button,
md-outlined-button,
md-elevated-button,
md-text-button,
md-filled-tonal-button {
color: var(--md-filled-button-disabled-label-text-color, #44474e);
width: 100%;
}
}
}
@@ -0,0 +1,153 @@
import { Component, Input, Output, EventEmitter, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
// Import Material Web Components
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/button/elevated-button.js';
import '@material/web/button/text-button.js';
import '@material/web/button/filled-tonal-button.js';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export type M3ButtonVariant = 'filled' | 'outlined' | 'elevated' | 'text' | 'tonal';
/**
* Material Design 3 Button Component
*
* Wrapper for @material/web button components following Material Design 3 specifications.
* Uses native Material Design 3 web components with full theming support.
*
* Features:
* - Five button variants: filled, outlined, elevated, text, and tonal
* - Automatic light/dark theme support
* - Proper disabled states with gray colors
* - Hover, focus, and pressed states with ripple effects
* - Full width support
* - Icon support (leading and trailing)
*
* @example
* // Basic usage with label input
* <mifosx-m3-button
* variant="filled"
* [label]="'Submit'"
* (clicked)="onSubmit()">
* </mifosx-m3-button>
*
* @example
* // With content projection
* <mifosx-m3-button variant="outlined" (clicked)="onCancel()">
* Cancel
* </mifosx-m3-button>
*
* @example
* // Full width disabled button
* <mifosx-m3-button
* variant="filled"
* [label]="'Loading...'"
* [disabled]="true"
* [fullWidth]="true">
* </mifosx-m3-button>
*
* @example
* // With translated label
* <mifosx-m3-button
* variant="tonal"
* type="submit"
* [label]="'labels.buttons.Save' | translate"
* (clicked)="onSave()">
* </mifosx-m3-button>
*/
@Component({
selector: 'mifosx-m3-button',
templateUrl: './m3-button.component.html',
styleUrls: ['./m3-button.component.scss'],
standalone: true,
imports: [
STANDALONE_SHARED_IMPORTS,
CommonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
encapsulation: ViewEncapsulation.None
})
export class M3ButtonComponent {
/**
* Button variant
* - filled: Solid background (default, highest emphasis)
* - outlined: Bordered button (medium emphasis)
* - elevated: Raised button with shadow (medium emphasis)
* - text: Text-only button (low emphasis)
* - tonal: Subtle background button (medium emphasis)
*/
@Input() variant: M3ButtonVariant = 'filled';
/**
* Button type attribute
*/
@Input() type: 'button' | 'submit' | 'reset' = 'button';
/**
* Disabled state
*/
@Input() disabled: boolean = false;
/**
* Button label text
*/
@Input() label?: string;
/**
* Icon to display before text
*/
@Input() icon?: string;
/**
* Icon to display after text
*/
@Input() trailingIcon?: string;
/**
* Whether the button should take full width
*/
@Input() fullWidth: boolean = false;
/**
* Click event emitter
*/
@Output() clicked = new EventEmitter<Event>();
/**
* Handle button click
*/
onClick(event: Event): void {
if (!this.disabled) {
this.clicked.emit(event);
}
}
/**
* Get the Material Web Component tag name based on variant
*/
get componentTag(): string {
switch (this.variant) {
case 'filled':
return 'md-filled-button';
case 'outlined':
return 'md-outlined-button';
case 'elevated':
return 'md-elevated-button';
case 'text':
return 'md-text-button';
case 'tonal':
return 'md-filled-tonal-button';
default:
return 'md-filled-button';
}
}
/**
* Get container classes
*/
get containerClasses(): string {
return this.fullWidth ? 'm3-button-container--full-width' : 'm3-button-container';
}
}
@@ -0,0 +1 @@
<span [class]="iconClass" [ngStyle]="iconStyles" class="m3-icon">{{ name }}</span>
@@ -0,0 +1,71 @@
// Material Symbols Font Faces
// These will be loaded from Google Fonts or local files
@use 'theme/material-web' as *;
.m3-icon {
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
vertical-align: middle;
// Smooth rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizelegibility;
}
// Material Symbols base styles
.material-symbols-outlined,
.material-symbols-filled,
.material-symbols-rounded,
.material-symbols-sharp {
font-family: 'Material Symbols Outlined', sans-serif;
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
color: var(--md-icon-prefix-label-text-color, #44474e);
}
// Filled variant
.material-symbols-filled {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Outlined variant (default)
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Rounded variant
.material-symbols-rounded {
font-family: 'Material Symbols Rounded', sans-serif;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Sharp variant
.material-symbols-sharp {
font-family: 'Material Symbols Sharp', sans-serif;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
@@ -0,0 +1,95 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
export type M3IconStyle = 'outlined' | 'filled' | 'rounded' | 'sharp';
/**
* Material Design 3 Icon Component
*
* Wrapper for Material Symbols icons following Material Design 3 specifications.
* Uses Google's Material Symbols font for consistent iconography.
*
* Features:
* - Multiple icon styles: outlined (default), filled, rounded, and sharp
* - Automatic theme support
* - Size control
* - Color customization via CSS
*
* @example
* // Basic usage
* <mifosx-m3-icon name="account_circle"></mifosx-m3-icon>
*
* @example
* // With filled style
* <mifosx-m3-icon name="lock" style="filled"></mifosx-m3-icon>
*
* @example
* // With custom size
* <mifosx-m3-icon name="visibility" [size]="24"></mifosx-m3-icon>
*
* Common icon mappings from Font Awesome:
* - user-circle → account_circle
* - lock → lock
* - eye → visibility
* - eye-slash → visibility_off
* - home → home
* - search → search
* - settings → settings
* - menu → menu
* - close → close
*/
@Component({
selector: 'mifosx-m3-icon',
templateUrl: './m3-icon.component.html',
styleUrls: ['./m3-icon.component.scss'],
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None
})
export class M3IconComponent {
/**
* Icon name from Material Symbols
* See: https://fonts.google.com/icons
*/
@Input() name!: string;
/**
* Icon style variant
* - outlined: Default style with strokes (default)
* - filled: Solid filled icons
* - rounded: Rounded corner icons
* - sharp: Sharp corner icons
*/
@Input() style: M3IconStyle = 'outlined';
/**
* Icon size in pixels
* Default is 24px (Material Design standard)
*/
@Input() size: number = 24;
/**
* Whether the icon should be filled (alternative to style="filled")
* This is a convenience property for backwards compatibility
*/
@Input() filled: boolean = false;
/**
* Get the CSS class for the icon style
*/
get iconClass(): string {
const styleToUse = this.filled ? 'filled' : this.style;
return `material-symbols-${styleToUse}`;
}
/**
* Get the inline styles for the icon
*/
get iconStyles(): { [key: string]: string } {
return {
'font-size': `${this.size}px`,
width: `${this.size}px`,
height: `${this.size}px`
};
}
}
@@ -1,11 +1,14 @@
@if (existMoreThanOneServer) {
<mat-form-field id="server-selector">
<mat-label>{{ 'labels.inputs.Server' | translate }}</mat-label>
<mat-form-field id="server-selector" [appearance]="appearance" [class.outlined-variant]="appearance === 'outline'">
@if (showLabel) {
<mat-label>{{ 'labels.inputs.Server' | translate }}</mat-label>
}
<mat-select
[formControl]="serverSelector"
(selectionChange)="setServer()"
panelClass="server-panel"
panelWidth="null"
[placeholder]="!showLabel ? ('labels.inputs.Server' | translate) : ''"
>
<div class="sticky">
<form [formGroup]="form" (ngSubmit)="addNewServer()">
@@ -1,3 +1,138 @@
// ===== Base Server Selector Styles =====
#server-selector {
&.mat-mdc-form-field {
// Default styles (fill variant)
&:not(.outlined-variant) {
mat-form-field {
width: 96%;
}
}
// Outlined variant styles (used in login)
&.outlined-variant {
.mat-mdc-text-field-wrapper {
padding: 0;
}
.mat-mdc-form-field-flex {
align-items: center;
}
.mat-mdc-form-field-infix {
padding: 12px 0;
min-height: 48px;
}
// Outlined border styles with Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #79747e);
border-width: 1px;
}
}
// Hover state
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #1a1c1e);
}
}
}
// Focused state
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #1074b9);
border-width: 2px;
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #1074b9);
}
}
// Select styling
.mat-mdc-select {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #44474e);
}
// Label styling
.mat-mdc-floating-label {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
}
}
}
// Dark theme support for outlined variant
.dark-theme {
#server-selector.outlined-variant {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #938f99);
}
}
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.sticky {
background-color: var(--md-sys-color-surface-container, #2c2f36);
}
}
}
// ===== Common Styles =====
.actions {
display: flex;
align-items: center;
@@ -12,8 +147,8 @@
position: sticky;
z-index: 1; // appear above mat-option
top: 0; // fix at the top
background-color: white; // don't see mat-option when scrolling
box-shadow: 1px -2px 10px 0 grey; // shadow at the bottom
background-color: var(--md-sys-color-surface-container, white); // don't see mat-option when scrolling
box-shadow: 1px -2px 10px 0 rgb(0 0 0 / 10%); // shadow at the bottom
}
mat-form-field {
@@ -1,5 +1,5 @@
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnInit, inject, Input } from '@angular/core';
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { UntypedFormBuilder, Validators } from '@angular/forms';
@@ -27,6 +27,12 @@ export class ServerSelectorComponent implements OnInit {
dialog = inject(MatDialog);
private formBuilder = inject(UntypedFormBuilder);
/** Appearance of the form field (fill, outline). Defaults to 'fill'. */
@Input() appearance: 'fill' | 'outline' = 'fill';
/** Show label in the form field. Defaults to true. */
@Input() showLabel: boolean = true;
/** Input server. */
form: any;
@@ -1,7 +1,4 @@
<mat-form-field class="flex-fill align-center" id="tenant-selector">
<span matPrefix class="m-r-10">
<fa-icon icon="building" size="lg"></fa-icon>
</span>
<mat-form-field class="flex-fill align-center" id="tenant-selector" appearance="fill">
<mat-label>{{ 'labels.inputs.Tenant' | translate }}</mat-label>
<mat-select [formControl]="tenantSelector" (selectionChange)="setTenantIdentifier()" class="tenantselector">
@for (tenant of tenants; track tenant) {
@@ -10,4 +7,5 @@
</mat-option>
}
</mat-select>
<mifosx-m3-icon matIconPrefix name="domain"></mifosx-m3-icon>
</mat-form-field>
@@ -1,5 +1,2 @@
#tenant-selector,
.tenantselector {
width: 14rem;
margin-bottom: 0.6rem;
}
// Styles are now controlled by parent component (login-form)
// to ensure consistency with other form fields
@@ -3,7 +3,7 @@ import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { MatFormField, MatPrefix, MatLabel } from '@angular/material/form-field';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { M3IconComponent } from 'app/shared/m3-ui/m3-icon/m3-icon.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -13,7 +13,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatPrefix,
FaIconComponent
M3IconComponent
]
})
export class TenantSelectorComponent implements OnInit {
@@ -1,3 +1,3 @@
<button mat-icon-button (click)="toggleTheme()" class="m-l-15 m-r-5">
<mat-icon>{{ darkModeOn ? 'wb_sunny' : 'nights_stay' }}</mat-icon>
<button mat-icon-button (click)="toggleTheme()" class="m-l-5 m-r-5">
<mifosx-m3-icon [name]="darkModeOn ? 'wb_sunny' : 'nights_stay'"></mifosx-m3-icon>
</button>
@@ -2,7 +2,7 @@ import { Component, OnChanges, OnInit, SimpleChanges, inject } from '@angular/co
import { ThemingService } from './theming.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { M3IconComponent } from '../m3-ui/m3-icon/m3-icon.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -12,7 +12,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIconButton,
MatIcon
M3IconComponent
]
})
export class ThemeToggleComponent implements OnInit, OnChanges {
@@ -29,8 +29,16 @@ export class ThemeToggleComponent implements OnInit, OnChanges {
this.darkModeOn = !!this.settingsService.themeDarkEnabled;
}
/**
* Toggle between light and dark themes
* This method handles the complete theme switching process:
* 1. Toggles the local state
* 2. Persists the preference to settings
*/
toggleTheme() {
// Step 1: Toggle the dark mode state
this.darkModeOn = !this.darkModeOn;
// Step 2: Persist the theme preference to localStorage via settings service
this.settingsService.setThemeDarkEnabled(this.darkModeOn);
this.themingService.setDarkMode(this.darkModeOn);
}
@@ -41,8 +41,10 @@ export class ThemingService {
this.darkModeOn = isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
this.theme.next('dark-theme');
} else {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
this.theme.next('light-theme');
}
+1
View File
@@ -51,4 +51,5 @@ export const STANDALONE_SHARED_IMPORTS = [
NgxTranslatePipe,
TranslatePipe
];
export { M3ButtonComponent } from './shared/m3-ui/m3-button/m3-button.component';
export { HasPermissionDirective } from './directives/has-permission/has-permission.directive';
@@ -27,8 +27,7 @@
>
@if (passwordInputType === 'password') {
<fa-icon icon="eye"></fa-icon>
}
@if (passwordInputType === 'text') {
} @else {
<fa-icon icon="eye-slash"></fa-icon>
}
</button>
+5
View File
@@ -3,19 +3,24 @@
// BackEnd Environment variables
window['env']['fineractApiUrls'] = '$FINERACT_API_URLS';
window['env']['fineractApiUrl'] = '$FINERACT_API_URL';
window['env']['apiProvider'] = '$FINERACT_API_PROVIDER';
window['env']['apiVersion'] = '$FINERACT_API_VERSION';
window['env']['apiActuator'] = '$FINERACT_API_ACTUATOR';
window['env']['fineractPlatformTenantId'] = '$FINERACT_PLATFORM_TENANT_IDENTIFIER';
window['env']['fineractPlatformTenantIds'] = '$FINERACT_PLATFORM_TENANTS_IDENTIFIER';
window['env']['tenantLogoUrl'] = '$TENANT_LOGO_URL';
// Language Environment variables
window['env']['defaultLanguage'] = '$MIFOS_DEFAULT_LANGUAGE';
window['env']['supportedLanguages'] = '$MIFOS_SUPPORTED_LANGUAGES';
window['env']['preloadClients'] = '$MIFOS_PRELOAD_CLIENTS';
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "Vybrat vše",
"Show more": "Zobrazit více",
"Show less": "Ukaž méně",
"Signing in...": "Přihlašování",
"Staff": "Personál",
"Staff Assignment History": "Historie přidělení zaměstnanců",
"Subledger Account": "Účet vedlejší knihy",
@@ -1242,7 +1243,10 @@
"Not Yet Amortized Amount": "Dosud neamortizovana castka",
"Adjusted Amount": "Upravená částka",
"Charged Off Amount": "Odepsaná částka",
"Date": "Datum"
"Date": "Datum",
"Secure & Reliable": "Bezpečné a spolehlivé",
"Community Driven": "Komunitně řízené",
"Global Impact": "Globální dopad"
},
"inputs": {
"accounting": {
@@ -2725,7 +2729,9 @@
},
"placeholders": {
"Add new server": "Přidat nový server",
"Search Activity": "Aktivita vyhledávání"
"Search Activity": "Aktivita vyhledávání",
"Enter your username": "Zadejte své uživatelské jméno",
"Enter your password": "Zadejte své heslo"
},
"status": {
"Active": "Aktivní",
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "Wählen Sie Alle",
"Show more": "Zeig mehr",
"Show less": "Zeige weniger",
"Signing in...": "Anmelden",
"Staff": "Personal",
"Staff Assignment History": "Verlauf der Personalzuweisungen",
"Subledger Account": "Nebenbuchkonto",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Noch nicht amortisierter Betrag",
"Adjusted Amount": "Angepasster Betrag",
"Charged Off Amount": "Abgeschriebener Betrag",
"Date": "Datum"
"Date": "Datum",
"Secure & Reliable": "Sicher und zuverlässig",
"Community Driven": "Gemeinschaftsorientiert",
"Global Impact": "Globale Auswirkungen"
},
"inputs": {
"accounting": {
@@ -2725,7 +2729,9 @@
},
"placeholders": {
"Add new server": "Neuen Server hinzufügen",
"Search Activity": "Suchaktivität"
"Search Activity": "Suchaktivität",
"Enter your username": "Geben Sie Ihren Benutzernamen ein",
"Enter your password": "Geben Sie Ihr Passwort ein"
},
"status": {
"Active": "Aktiv",
+8 -2
View File
@@ -506,6 +506,7 @@
"Select All": "Select All",
"Show less": "Show less",
"Show more": "Show more",
"Signing in...": "Signing in...",
"Staff": "Staff",
"Staff Assignment History": "Staff Assignment History",
"Subledger Account": "Subledger Account",
@@ -1247,7 +1248,10 @@
"Not Yet Amortized Amount": "Not Yet Amortized Amount",
"Adjusted Amount": "Adjusted Amount",
"Charged Off Amount": "Charged Off Amount",
"Date": "Date"
"Date": "Date",
"Secure & Reliable": "Secure & Reliable",
"Community Driven": "Community Driven",
"Global Impact": "Global Impact"
},
"inputs": {
"accounting": {
@@ -2735,7 +2739,9 @@
},
"placeholders": {
"Add new server": "Add new server",
"Search Activity": "Search Activity"
"Search Activity": "Search Activity",
"Enter your username": "Enter your username",
"Enter your password": "Enter your password"
},
"status": {
"Active": "Active",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "Seleccionar todo",
"Show more": "Mostrar más",
"Show less": "Muestra menos",
"Signing in...": "Iniciando sesión",
"Staff": "Asesor",
"Staff Assignment History": "Historial de asignaciones de asesor",
"Subledger Account": "Cuenta auxiliar",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Monto aun no amortizado",
"Adjusted Amount": "Importe ajustado",
"Charged Off Amount": "Importe cancelado",
"Date": "Fecha"
"Date": "Fecha",
"Secure & Reliable": "Seguro y Confiable",
"Community Driven": "Impulsado por la Comunidad",
"Global Impact": "Impacto Global"
},
"inputs": {
"accounting": {
@@ -2724,7 +2728,9 @@
},
"placeholders": {
"Add new server": "Agregar nuevo servidor",
"Search Activity": "Actividad de búsqueda"
"Search Activity": "Actividad de búsqueda",
"Enter your username": "Ingrese su nombre de usuario",
"Enter your password": "Ingrese su contraseña"
},
"status": {
"Active": "Activo",
@@ -2912,7 +2918,6 @@
"Create Recurring Deposits Account": "Crear cuenta de depósitos recurrentes",
"Create Report": "Crear reporte",
"Create SMS Campaign": "Crear campaña de SMS",
"Create Savings Product": "Crear producto de ahorro",
"Create Savings Account": "Crear Cuenta de Ahorros",
"Create Self Service User": "Crear usuario de autoservicio",
"Create Share Product": "Crear producto de Acciones",
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "Seleccionar todo",
"Show more": "Mostrar más",
"Show less": "Muestra menos",
"Signing in...": "Iniciando sesión",
"Staff": "Asesor",
"Staff Assignment History": "Historial de asignaciones de asesor",
"Subledger Account": "Cuenta auxiliar",
@@ -1242,7 +1243,10 @@
"Not Yet Amortized Amount": "Monto aun no amortizado",
"Adjusted Amount": "Importe ajustado",
"Charged Off Amount": "Importe cancelado",
"Date": "Fecha"
"Date": "Fecha",
"Secure & Reliable": "Seguro y Confiable",
"Community Driven": "Impulsado por la Comunidad",
"Global Impact": "Impacto Global"
},
"inputs": {
"accounting": {
@@ -2727,7 +2731,9 @@
},
"placeholders": {
"Add new server": "Agregar nuevo servidor",
"Search Activity": "Actividad de búsqueda"
"Search Activity": "Actividad de búsqueda",
"Enter your username": "Ingrese su nombre de usuario",
"Enter your password": "Ingrese su contraseña"
},
"status": {
"Active": "Activo",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "Tout sélectionner",
"Show more": "Montre plus",
"Show less": "Montrer moins",
"Signing in...": "Connexion",
"Staff": "Personnel",
"Staff Assignment History": "Historique des affectations du personnel",
"Subledger Account": "Compte auxiliaire",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Montant pas encore amorti",
"Adjusted Amount": "Montant ajusté",
"Charged Off Amount": "Montant radié",
"Date": "Date"
"Date": "Date",
"Secure & Reliable": "Sécurisé et fiable",
"Community Driven": "Axé sur la communauté",
"Global Impact": "Impact mondial"
},
"inputs": {
"accounting": {
@@ -2723,7 +2727,9 @@
},
"placeholders": {
"Add new server": "Ajouter un nouveau serveur",
"Search Activity": "Activité de recherche"
"Search Activity": "Activité de recherche",
"Enter your username": "Saisissez votre nom d'utilisateur",
"Enter your password": "Saisissez votre mot de passe"
},
"status": {
"Active": "Actif",
@@ -2911,7 +2917,6 @@
"Create Recurring Deposits Account": "Créer un compte de dépôts récurrents",
"Create Report": "Creer un rapport",
"Create SMS Campaign": "Créer une campagne SMS",
"Create Savings Product": "Créer un produit d'épargne",
"Create Savings Account": "Créer un compte d'épargne",
"Create Self Service User": "Créer un utilisateur libre-service",
"Create Share Product": "Créer un produit partagé",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "Seleziona tutto",
"Show more": "Mostra di più",
"Show less": "Mostra meno",
"Signing in...": "Accesso",
"Staff": "Personale",
"Staff Assignment History": "Cronologia delle assegnazioni del personale",
"Subledger Account": "Conto secondario",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Importo non ancora ammortizzato",
"Adjusted Amount": "Importo rettificato",
"Charged Off Amount": "Importo stornato",
"Date": "Data"
"Date": "Data",
"Secure & Reliable": "Sicuro e affidabile",
"Community Driven": "Guidato dalla comunità",
"Global Impact": "Impatto globale"
},
"inputs": {
"accounting": {
@@ -2723,7 +2727,9 @@
},
"placeholders": {
"Add new server": "Aggiungi nuovo server",
"Search Activity": "Attività di ricerca"
"Search Activity": "Attività di ricerca",
"Enter your username": "Inserisci il tuo nome utente",
"Enter your password": "Inserisci la tua password"
},
"status": {
"Active": "Attivo",
@@ -2911,7 +2917,6 @@
"Create Recurring Deposits Account": "Crea un conto per depositi ricorrenti",
"Create Report": "Crea rapporto",
"Create SMS Campaign": "Crea campagna SMS",
"Create Savings Product": "Crea prodotto salvabile",
"Create Savings Account": "Crea un conto di risparmio",
"Create Self Service User": "Crea utente self-service",
"Create Share Product": "Crea prodotto condiviso",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "모두 선택",
"Show more": "자세히보기",
"Show less": "간략히 보기",
"Signing in...": "로그인",
"Staff": "직원",
"Staff Assignment History": "직원 배정 내역",
"Subledger Account": "보조원장 계정",
@@ -1244,7 +1245,10 @@
"Not Yet Amortized Amount": "Not Yet Amortized Amount",
"Adjusted Amount": "조정된 금액",
"Charged Off Amount": "상각 금액",
"Date": "날짜"
"Date": "날짜",
"Secure & Reliable": "안전하고 신뢰할 수 있음",
"Community Driven": "커뮤니티 주도형",
"Global Impact": "글로벌 영향"
},
"inputs": {
"accounting": {
@@ -2723,7 +2727,9 @@
},
"placeholders": {
"Add new server": "새 서버 추가",
"Search Activity": "검색 활동"
"Search Activity": "검색 활동",
"Enter your username": "사용자 이름을 입력하세요",
"Enter your password": "비밀번호를 입력하세요"
},
"status": {
"Active": "활동적인",
@@ -2911,7 +2917,6 @@
"Create Recurring Deposits Account": "정기예금 계좌 생성",
"Create Report": "보고서 작성",
"Create SMS Campaign": "SMS 캠페인 생성",
"Create Savings Product": "저장상품 생성",
"Create Savings Account": "저축 계좌 만들기",
"Create Self Service User": "셀프 서비스 사용자 생성",
"Create Share Product": "공유상품 생성",
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "Pasirinkti viską",
"Show more": "Rodyti daugiau",
"Show less": "Rodyti mažiau",
"Signing in...": "Prisijungimas",
"Staff": "Personalas",
"Staff Assignment History": "Darbuotojų paskyrimo istorija",
"Subledger Account": "Subledger sąskaita",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Dar neamortizuota suma",
"Adjusted Amount": "Koreguota suma",
"Charged Off Amount": "Nurašyta suma",
"Date": "Data"
"Date": "Data",
"Secure & Reliable": "Saugu ir patikima",
"Community Driven": "Bendruomenės varomas",
"Global Impact": "Pasaulinis poveikis"
},
"inputs": {
"accounting": {
@@ -2725,7 +2729,9 @@
},
"placeholders": {
"Add new server": "Pridėti naują serverį",
"Search Activity": "Paieškos veikla"
"Search Activity": "Paieškos veikla",
"Enter your username": "Įveskite savo vartotojo vardą",
"Enter your password": "Įveskite slaptažodį"
},
"status": {
"Active": "Aktyvus",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "Izvēlēties visus",
"Show more": "Parādīt vairāk",
"Show less": "Rādīt mazāk",
"Signing in...": "Pierakstīšanās",
"Staff": "Personāls",
"Staff Assignment History": "Personāla norīkošanas vēsture",
"Subledger Account": "Subledger konts",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Vel nav amortizeta summa",
"Adjusted Amount": "Koriģētā summa",
"Charged Off Amount": "Norakstītā summa",
"Date": "Datums"
"Date": "Datums",
"Secure & Reliable": "Drošs un uzticams",
"Community Driven": "Kopienas vadīts",
"Global Impact": "Globālā ietekme"
},
"inputs": {
"accounting": {
@@ -2723,7 +2727,9 @@
},
"placeholders": {
"Add new server": "Pievienot jaunu serveri",
"Search Activity": "Meklēšanas darbība"
"Search Activity": "Meklēšanas darbība",
"Enter your username": "Ievadiet savu lietotājvārdu",
"Enter your password": "Ievadiet savu paroli"
},
"status": {
"Active": "Aktīvs",
@@ -2803,7 +2809,6 @@
"All Recurring Deposits": "Visi atkārtotie noguldījumi",
"All Savings": "Visi ietaupījumi",
"Savings Account Details": "Krājkonta informācija",
"Create Savings Product": "Izveidot uzkrājumu produktu",
"Allocate Cash": "Piešķirt skaidru naudu",
"Allows you to create new fixed deposit product": "Šī opcija ļauj izveidot jaunu fiksētā depozīta produktu.",
"This option allows you to create new users in your organization": "Šī opcija ļauj izveidot jaunus lietotājus jūsu organizācijā",
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "सबै छान्नु",
"Show more": "थप देखाउनुहोस्",
"Show less": "कम देखाउनुहोस्",
"Signing in...": "साइन इन गर्दै",
"Staff": "कर्मचारी",
"Staff Assignment History": "कर्मचारी असाइनमेन्ट इतिहास",
"Subledger Account": "Subledger खाता",
@@ -1241,7 +1242,10 @@
"Not Yet Amortized Amount": "Not Yet Amortized Amount",
"Adjusted Amount": "समायोजित रकम",
"Charged Off Amount": "चार्ज गरिएको रकम",
"Date": "मिति"
"Date": "मिति",
"Secure & Reliable": "सुरक्षित र भरपर्दो",
"Community Driven": "समुदाय संचालित",
"Global Impact": "विश्वव्यापी प्रभाव"
},
"inputs": {
"accounting": {
@@ -2721,7 +2725,9 @@
},
"placeholders": {
"Add new server": "नयाँ सर्भर थप्नुहोस्",
"Search Activity": "खोज गतिविधि"
"Search Activity": "खोज गतिविधि",
"Enter your username": "आफ्नो प्रयोगकर्ता नाम प्रविष्ट गर्नुहोस्",
"Enter your password": "आफ्नो पासवर्ड प्रविष्ट गर्नुहोस्"
},
"status": {
"Active": "सक्रिय",
+8 -3
View File
@@ -505,6 +505,7 @@
"Select All": "Selecionar tudo",
"Show more": "Mostre mais",
"Show less": "Mostre menos",
"Signing in...": "Entrando",
"Staff": "Funcionários",
"Staff Assignment History": "Histórico de Atribuição de Pessoal",
"Subledger Account": "Conta auxiliar",
@@ -1243,7 +1244,10 @@
"Not Yet Amortized Amount": "Valor ainda nao amortizado",
"Adjusted Amount": "Valor ajustado",
"Charged Off Amount": "Valor baixado",
"Date": "Data"
"Date": "Data",
"Secure & Reliable": "Seguro e confiável",
"Community Driven": "Impulsionado pela comunidade",
"Global Impact": "Impacto global"
},
"inputs": {
"accounting": {
@@ -2723,7 +2727,9 @@
},
"placeholders": {
"Add new server": "Adicionar novo servidor",
"Search Activity": "Atividade de pesquisa"
"Search Activity": "Atividade de pesquisa",
"Enter your username": "Digite seu nome de usuário",
"Enter your password": "Digite sua senha"
},
"status": {
"Active": "Ativo",
@@ -2911,7 +2917,6 @@
"Create Recurring Deposits Account": "Criar conta de depósitos recorrentes",
"Create Report": "Criar relatório",
"Create SMS Campaign": "Criar campanha SMS",
"Create Savings Product": "Criar produto salvador",
"Create Savings Account": "Criar conta poupança",
"Create Self Service User": "Criar usuário de autoatendimento",
"Create Share Product": "Criar produto compartilhado",
+8 -2
View File
@@ -505,6 +505,7 @@
"Select All": "Chagua Zote",
"Show more": "Onyesha zaidi",
"Show less": "Onyesha kidogo",
"Signing in...": "Kuingia",
"Staff": "Wafanyakazi",
"Staff Assignment History": "Historia ya Kazi ya Wafanyakazi",
"Subledger Account": "Akaunti ya Subleja",
@@ -1241,7 +1242,10 @@
"Not Yet Amortized Amount": "Kiasi Bado Kijaondolewa",
"Adjusted Amount": "Kiasi Kilichorekebishwa",
"Charged Off Amount": "Kiasi Kilichofutwa",
"Date": "Tarehe"
"Date": "Tarehe",
"Secure & Reliable": "Salama na ya Kuaminika",
"Community Driven": "Inaendeshwa na Jumuiya",
"Global Impact": "Athari ya Kimataifa"
},
"inputs": {
"accounting": {
@@ -2721,7 +2725,9 @@
},
"placeholders": {
"Add new server": "Ongeza seva mpya",
"Search Activity": "Shughuli ya Utafutaji"
"Search Activity": "Shughuli ya Utafutaji",
"Enter your username": "Ingiza jina lako la mtumiaji",
"Enter your password": "Ingiza nenosiri lako"
},
"status": {
"Active": "Inayotumika",
+4
View File
@@ -14,6 +14,10 @@
<link rel="manifest" href="manifest.json" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
rel="stylesheet"
/>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Mono:300" rel="stylesheet" />
<!-- Load environment variables -->
</head>
+60
View File
@@ -1,4 +1,5 @@
@use 'theme/mifosx-theme';
@use 'theme/material-web' as *;
@use 'theme/pictonblue-yellowgreen' as *;
@use 'theme/indigo-pink' as *;
@use 'theme/pink-bluegrey' as *;
@@ -24,6 +25,65 @@
z-index: 9999;
}
// ===== Material Select Panel Styles (Material Design 3) =====
.mat-mdc-select-panel {
background: var(--md-sys-color-surface-container, #fff);
border-radius: 8px;
box-shadow:
0 2px 4px #00000014,
0 4px 12px #0000000f,
0 8px 24px #00000014;
.mat-mdc-option {
font-size: 0.875rem;
min-height: 48px;
padding: 0 16px;
color: var(--md-sys-color-on-surface, #1a1c1e);
transition: background-color 0.2s ease;
&:hover {
background-color: #1074b914;
}
&.mat-mdc-option-active {
background-color: #1074b91f;
}
&.mdc-list-item--selected {
background-color: #1074b91f;
color: var(--md-sys-color-primary, #1074b9);
font-weight: 500;
}
.mdc-list-item__primary-text {
color: inherit;
}
}
}
// Dark theme select panel
.dark-theme .mat-mdc-select-panel {
background: var(--md-sys-color-surface-container, #2c2f36);
.mat-mdc-option {
color: var(--md-sys-color-on-surface, #e2e2e5);
&:hover {
background-color: #5ba2ec14;
}
&.mat-mdc-option-active {
background-color: #5ba2ec1f;
}
&.mdc-list-item--selected {
background-color: #5ba2ec1f;
color: var(--md-sys-color-primary, #5ba2ec);
font-weight: 500;
}
}
}
.scrollbar-styling {
scrollbar-width: thin;
scrollbar-color: #889 #f1f1f1;
+1 -1
View File
@@ -258,7 +258,7 @@ body.dark-theme {
}
.copy-label {
color: white !important;
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
p {
color: white !important;
+317
View File
@@ -0,0 +1,317 @@
/**
* Material Web Components Global Styles
* These styles apply to @material/web components throughout the app
* Following Material Design 3 specifications
*/
// Material Web Button Components base styling
md-filled-button,
md-outlined-button,
md-elevated-button,
md-text-button,
md-filled-tonal-button {
// Ensure buttons are properly displayed
display: inline-flex;
align-items: center;
justify-content: center;
// Ensure minimum height for consistency
min-height: 40px;
// Typography
--md-filled-button-label-text-size: 14px;
--md-filled-button-label-text-weight: 500;
--md-filled-button-label-text-font: 'Roboto', sans-serif;
--md-outlined-button-label-text-size: 14px;
--md-outlined-button-label-text-weight: 500;
--md-outlined-button-label-text-font: 'Roboto', sans-serif;
--md-elevated-button-label-text-size: 14px;
--md-elevated-button-label-text-weight: 500;
--md-elevated-button-label-text-font: 'Roboto', sans-serif;
--md-text-button-label-text-size: 14px;
--md-text-button-label-text-weight: 500;
--md-text-button-label-text-font: 'Roboto', sans-serif;
--md-filled-tonal-button-label-text-size: 14px;
--md-filled-tonal-button-label-text-weight: 500;
--md-filled-tonal-button-label-text-font: 'Roboto', sans-serif;
// Border radius (Material Design 3 uses 20px for buttons)
--md-filled-button-container-shape: 20px;
--md-outlined-button-container-shape: 20px;
--md-elevated-button-container-shape: 20px;
--md-text-button-container-shape: 20px;
--md-filled-tonal-button-container-shape: 20px;
// Padding
--md-filled-button-leading-space: 24px;
--md-filled-button-trailing-space: 24px;
--md-outlined-button-leading-space: 24px;
--md-outlined-button-trailing-space: 24px;
--md-elevated-button-leading-space: 24px;
--md-elevated-button-trailing-space: 24px;
--md-text-button-leading-space: 24px;
--md-text-button-trailing-space: 24px;
--md-filled-tonal-button-leading-space: 24px;
--md-filled-tonal-button-trailing-space: 24px;
// Apply theme colors using Material Design tokens
--md-sys-color-primary: #1074b9;
--md-sys-color-on-primary: #fff;
--md-sys-color-surface-container-highest: #e1e2ec;
--md-sys-color-on-surface: #303135;
--md-sys-color-on-surface-variant: #44474e;
// ========== FILLED BUTTON ==========
// Normal state
--md-filled-button-container-color: #1074b9;
--md-filled-button-label-text-color: #fff;
// Hover state
--md-filled-button-hover-state-layer-color: #fff;
--md-filled-button-hover-state-layer-opacity: 8%;
// Focus state
--md-filled-button-focus-state-layer-color: #fff;
--md-filled-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-filled-button-pressed-state-layer-color: #fff;
--md-filled-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-filled-button-disabled-container-color: #e0e0e0;
--md-filled-button-disabled-container-opacity: 1;
--md-filled-button-disabled-label-text-color: #9e9e9e;
--md-filled-button-disabled-label-text-opacity: 1;
// ========== OUTLINED BUTTON ==========
// Normal state
--md-outlined-button-outline-color: #1074b9;
--md-outlined-button-label-text-color: #1074b9;
--md-outlined-button-outline-width: 1px;
// Hover state
--md-outlined-button-hover-state-layer-color: #1074b9;
--md-outlined-button-hover-state-layer-opacity: 8%;
// Focus state
--md-outlined-button-focus-state-layer-color: #1074b9;
--md-outlined-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-outlined-button-pressed-state-layer-color: #1074b9;
--md-outlined-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-outlined-button-disabled-outline-color: #e0e0e0;
--md-outlined-button-disabled-outline-opacity: 1;
--md-outlined-button-disabled-label-text-color: #9e9e9e;
--md-outlined-button-disabled-label-text-opacity: 1;
// ========== ELEVATED BUTTON ==========
// Normal state
--md-elevated-button-container-color: #f5f5f5;
--md-elevated-button-label-text-color: #1074b9;
--md-elevated-button-container-elevation: 1;
// Hover state
--md-elevated-button-hover-state-layer-color: #1074b9;
--md-elevated-button-hover-state-layer-opacity: 8%;
--md-elevated-button-hover-container-elevation: 2;
// Focus state
--md-elevated-button-focus-state-layer-color: #1074b9;
--md-elevated-button-focus-state-layer-opacity: 12%;
--md-elevated-button-focus-container-elevation: 1;
// Pressed state
--md-elevated-button-pressed-state-layer-color: #1074b9;
--md-elevated-button-pressed-state-layer-opacity: 12%;
--md-elevated-button-pressed-container-elevation: 1;
// Disabled state
--md-elevated-button-disabled-container-color: #e0e0e0;
--md-elevated-button-disabled-container-opacity: 1;
--md-elevated-button-disabled-container-elevation: 0;
--md-elevated-button-disabled-label-text-color: #9e9e9e;
--md-elevated-button-disabled-label-text-opacity: 1;
// ========== TEXT BUTTON ==========
// Normal state
--md-text-button-label-text-color: #1074b9;
// Hover state
--md-text-button-hover-state-layer-color: #1074b9;
--md-text-button-hover-state-layer-opacity: 8%;
// Focus state
--md-text-button-focus-state-layer-color: #1074b9;
--md-text-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-text-button-pressed-state-layer-color: #1074b9;
--md-text-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-text-button-disabled-label-text-color: #9e9e9e;
--md-text-button-disabled-label-text-opacity: 1;
// ========== TONAL BUTTON ==========
// Normal state
--md-filled-tonal-button-container-color: #d1e4ff;
--md-filled-tonal-button-label-text-color: #001d36;
// Hover state
--md-filled-tonal-button-hover-state-layer-color: #001d36;
--md-filled-tonal-button-hover-state-layer-opacity: 8%;
// Focus state
--md-filled-tonal-button-focus-state-layer-color: #001d36;
--md-filled-tonal-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-filled-tonal-button-pressed-state-layer-color: #001d36;
--md-filled-tonal-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-filled-tonal-button-disabled-container-color: #e0e0e0;
--md-filled-tonal-button-disabled-container-opacity: 1;
--md-filled-tonal-button-disabled-label-text-color: #9e9e9e;
--md-filled-tonal-button-disabled-label-text-opacity: 1;
}
// Dark theme support
.dark-theme {
--mat-app-text-color: #e2e2e5;
--md-sys-color-surface: #424242;
--md-sys-color-surface-container-low: #424242;
--md-sys-color-on-surface: #e2e2e5;
// Icons Prefix - Global definition for dark theme
--md-icon-prefix-label-text-color: #e3e2e6;
md-filled-button,
md-outlined-button,
md-elevated-button,
md-text-button,
md-filled-tonal-button {
--md-sys-color-primary: #5ba2ec;
--md-sys-color-on-primary: #001d36;
--md-sys-color-surface-container-highest: #42464e;
--md-sys-color-on-surface: #424242;
--md-sys-color-on-surface-variant: #c4c6d0;
// ========== FILLED BUTTON DARK ==========
// Normal state
--md-filled-button-container-color: #5ba2ec;
--md-filled-button-label-text-color: #fff;
// Hover state
--md-filled-button-hover-state-layer-color: #001d36;
--md-filled-button-hover-state-layer-opacity: 8%;
// Focus state
--md-filled-button-focus-state-layer-color: #001d36;
--md-filled-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-filled-button-pressed-state-layer-color: #001d36;
--md-filled-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-filled-button-disabled-container-color: #e3e2e6;
--md-filled-button-disabled-container-opacity: 1;
--md-filled-button-disabled-label-text-color: #44474e;
--md-filled-button-disabled-label-text-opacity: 1;
// ========== OUTLINED BUTTON DARK ==========
// Normal state
--md-outlined-button-outline-color: #5ba2ec;
--md-outlined-button-label-text-color: #5ba2ec;
// Hover state
--md-outlined-button-hover-state-layer-color: #5ba2ec;
--md-outlined-button-hover-state-layer-opacity: 8%;
// Focus state
--md-outlined-button-focus-state-layer-color: #5ba2ec;
--md-outlined-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-outlined-button-pressed-state-layer-color: #5ba2ec;
--md-outlined-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-outlined-button-disabled-outline-color: #e3e2e6;
--md-outlined-button-disabled-outline-opacity: 1;
--md-outlined-button-disabled-label-text-color: #44474e;
--md-outlined-button-disabled-label-text-opacity: 1;
// ========== ELEVATED BUTTON DARK ==========
// Normal state
--md-elevated-button-container-color: #2c2f36;
--md-elevated-button-label-text-color: #5ba2ec;
--md-elevated-button-container-elevation: 1;
// Hover state
--md-elevated-button-hover-state-layer-color: #5ba2ec;
--md-elevated-button-hover-state-layer-opacity: 8%;
--md-elevated-button-hover-container-elevation: 2;
// Focus state
--md-elevated-button-focus-state-layer-color: #5ba2ec;
--md-elevated-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-elevated-button-pressed-state-layer-color: #5ba2ec;
--md-elevated-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-elevated-button-disabled-container-color: #e3e2e6;
--md-elevated-button-disabled-container-opacity: 1;
--md-elevated-button-disabled-container-elevation: 0;
--md-elevated-button-disabled-label-text-color: #44474e;
--md-elevated-button-disabled-label-text-opacity: 1;
// ========== TEXT BUTTON DARK ==========
// Normal state
--md-text-button-label-text-color: #5ba2ec;
// Hover state
--md-text-button-hover-state-layer-color: #5ba2ec;
--md-text-button-hover-state-layer-opacity: 8%;
// Focus state
--md-text-button-focus-state-layer-color: #5ba2ec;
--md-text-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-text-button-pressed-state-layer-color: #5ba2ec;
--md-text-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-text-button-disabled-label-text-color: #e3e2e6;
--md-text-button-disabled-label-text-opacity: 1;
// ========== TONAL BUTTON DARK ==========
// Normal state
--md-filled-tonal-button-container-color: #003a6b;
--md-filled-tonal-button-label-text-color: #d1e4ff;
// Hover state
--md-filled-tonal-button-hover-state-layer-color: #d1e4ff;
--md-filled-tonal-button-hover-state-layer-opacity: 8%;
// Focus state
--md-filled-tonal-button-focus-state-layer-color: #d1e4ff;
--md-filled-tonal-button-focus-state-layer-opacity: 12%;
// Pressed state
--md-filled-tonal-button-pressed-state-layer-color: #d1e4ff;
--md-filled-tonal-button-pressed-state-layer-opacity: 12%;
// Disabled state
--md-filled-tonal-button-disabled-container-color: #e3e2e6;
--md-filled-tonal-button-disabled-container-opacity: 1;
--md-filled-tonal-button-disabled-label-text-color: #e3e2e6;
--md-filled-tonal-button-disabled-label-text-opacity: 1;
}
}