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:
+6
-4
@@ -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-/"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
Generated
+223
-425
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "공유상품 생성",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ā",
|
||||
|
||||
@@ -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": "सक्रिय",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user